mirror of
https://github.com/kiwix/libkiwix.git
synced 2025-12-24 06:57:59 -05:00
Compare commits
540 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2e42ac65c | ||
|
|
4124ad30d5 | ||
|
|
3c5d73027d | ||
|
|
d88bdd3ebf | ||
|
|
5cfe34a5c2 | ||
|
|
ad133bc9a3 | ||
|
|
8eabae6286 | ||
|
|
f3c96b23fd | ||
|
|
a92e9d8756 | ||
|
|
8d39b2c4c1 | ||
|
|
6d237ff1d5 | ||
|
|
78083f1f4a | ||
|
|
dd60235010 | ||
|
|
e799f2ff1e | ||
|
|
312f2cb560 | ||
|
|
fa42cbc48f | ||
|
|
f1797993af | ||
|
|
f886c8c07b | ||
|
|
9ca6bd006f | ||
|
|
cdacc0caf1 | ||
|
|
dfad1c3815 | ||
|
|
07252a127a | ||
|
|
b60e3ffb26 | ||
|
|
70d42aec98 | ||
|
|
4aa3c792aa | ||
|
|
208dece7e3 | ||
|
|
19b59fd72f | ||
|
|
92c2de8d46 | ||
|
|
feeb9f206e | ||
|
|
a1520ce7f1 | ||
|
|
2e53b51696 | ||
|
|
b259afa408 | ||
|
|
3c3cf08a1a | ||
|
|
54b78eaf56 | ||
|
|
1e0ff1fbb0 | ||
|
|
5b272ac49c | ||
|
|
0a3d293ae0 | ||
|
|
86ef2e2199 | ||
|
|
a0332e7599 | ||
|
|
2ef488816c | ||
|
|
1ccafe2d97 | ||
|
|
d6c62b3cd3 | ||
|
|
f39c558d2a | ||
|
|
5b46ad5934 | ||
|
|
49dbd0aa52 | ||
|
|
179f0faeb1 | ||
|
|
bb92f26b60 | ||
|
|
3a4e8303a0 | ||
|
|
063bb8cd65 | ||
|
|
b54e5ab969 | ||
|
|
2632a21d24 | ||
|
|
5c97b1fff9 | ||
|
|
4f7175ad59 | ||
|
|
f4b8d0c303 | ||
|
|
188694f2a1 | ||
|
|
e2f6d91d51 | ||
|
|
c35f6f9142 | ||
|
|
7f0d3004c9 | ||
|
|
5567d8ca49 | ||
|
|
5315034afe | ||
|
|
3288cd80e5 | ||
|
|
56434de79e | ||
|
|
e9ba151e6f | ||
|
|
5f83944699 | ||
|
|
9c0ae835e2 | ||
|
|
e5fac30cee | ||
|
|
7ef08b670b | ||
|
|
2736a46cfe | ||
|
|
672b4fc907 | ||
|
|
012973d14a | ||
|
|
67984cca5b | ||
|
|
d4e35c7067 | ||
|
|
6e37cabaea | ||
|
|
c8b7f8772a | ||
|
|
fc7484ac86 | ||
|
|
c236f3a32b | ||
|
|
3c7faddb6e | ||
|
|
cd02b4de3b | ||
|
|
5188355878 | ||
|
|
05cc3d015f | ||
|
|
39b62c6108 | ||
|
|
9c43353b72 | ||
|
|
b82fff9855 | ||
|
|
68189de162 | ||
|
|
6aab9b6981 | ||
|
|
41276341d0 | ||
|
|
02c3dff142 | ||
|
|
be6b58c6ad | ||
|
|
ab0ffb55bc | ||
|
|
950e742116 | ||
|
|
69257610e8 | ||
|
|
700d4becb9 | ||
|
|
dd795bd56d | ||
|
|
5fdc51b23e | ||
|
|
32cc6b0dcb | ||
|
|
3879b82112 | ||
|
|
7336dcab1d | ||
|
|
63e9a09259 | ||
|
|
4178c169dd | ||
|
|
59e9a0cd77 | ||
|
|
f751aff2fb | ||
|
|
87dc9d2723 | ||
|
|
9c7366890d | ||
|
|
19e195cb7d | ||
|
|
3d5fd8f585 | ||
|
|
d3d5abe14d | ||
|
|
e805f68994 | ||
|
|
a759ab989f | ||
|
|
7ccd9ffcce | ||
|
|
0c0a37073b | ||
|
|
415c65cf03 | ||
|
|
8287f351e7 | ||
|
|
ea779ac200 | ||
|
|
80cd1fc989 | ||
|
|
2d76f8395e | ||
|
|
29a6a34ecf | ||
|
|
2f3f1a4859 | ||
|
|
b9be742085 | ||
|
|
95c354a5fa | ||
|
|
cdd272fc5a | ||
|
|
ef962a9174 | ||
|
|
f063d350c6 | ||
|
|
d8fe593f59 | ||
|
|
22b8625033 | ||
|
|
0f277ffa34 | ||
|
|
068f7e5e95 | ||
|
|
8c810d2d2f | ||
|
|
8c18a37961 | ||
|
|
db3e0d7f72 | ||
|
|
d134ad417f | ||
|
|
965b9622c2 | ||
|
|
11db5dec4e | ||
|
|
9d4370403b | ||
|
|
cb57178c23 | ||
|
|
9ba5ab4678 | ||
|
|
a597870025 | ||
|
|
611146aa37 | ||
|
|
6d2f227c42 | ||
|
|
0c7d19ab45 | ||
|
|
b54215f146 | ||
|
|
9033f2f28e | ||
|
|
5c289abd0e | ||
|
|
ec9186b174 | ||
|
|
aaaa5a637e | ||
|
|
49940a30d0 | ||
|
|
24ed96a38c | ||
|
|
ccdc316217 | ||
|
|
ba44033273 | ||
|
|
5cb276a933 | ||
|
|
e4be97a032 | ||
|
|
aa2a031ba4 | ||
|
|
803cb1c2c5 | ||
|
|
7872734f44 | ||
|
|
d061697de7 | ||
|
|
c557bb271b | ||
|
|
1f45c42c32 | ||
|
|
93264f7409 | ||
|
|
baed447dd3 | ||
|
|
20b487da8d | ||
|
|
e214efecd4 | ||
|
|
09233bf4f3 | ||
|
|
47c67a4202 | ||
|
|
6b600a18eb | ||
|
|
9e887cadf1 | ||
|
|
a599fb3892 | ||
|
|
a17fc0ef2d | ||
|
|
db06b2c7ca | ||
|
|
a20f9e2ce1 | ||
|
|
f7c867f8a7 | ||
|
|
b7b0bdbdd8 | ||
|
|
a870e05621 | ||
|
|
4abc4f8518 | ||
|
|
6b2067c236 | ||
|
|
e55bf514e8 | ||
|
|
80d4f7e349 | ||
|
|
f270724b1f | ||
|
|
58186ffb26 | ||
|
|
6d43fd065f | ||
|
|
071d2bedd3 | ||
|
|
0b1740e6c5 | ||
|
|
9913f748e2 | ||
|
|
c5c40cb189 | ||
|
|
ae32ff40c0 | ||
|
|
26331b401e | ||
|
|
0f368791a2 | ||
|
|
fb26f6b9c5 | ||
|
|
32643fbd94 | ||
|
|
faa9e1f8b5 | ||
|
|
bd781f8e8b | ||
|
|
c7d77395e7 | ||
|
|
a7fea462b0 | ||
|
|
89d7e68a39 | ||
|
|
67caae6c32 | ||
|
|
d3f2e08b35 | ||
|
|
839fc10a4f | ||
|
|
5a8b825c70 | ||
|
|
7a465e66d7 | ||
|
|
5a99634dfd | ||
|
|
e028bcbb04 | ||
|
|
9cdf7a44c0 | ||
|
|
4d23e44de7 | ||
|
|
98d69ef59b | ||
|
|
e40827fbac | ||
|
|
a798e0c0a1 | ||
|
|
971374e049 | ||
|
|
f7608c378e | ||
|
|
c13a43010a | ||
|
|
eea10ec3f5 | ||
|
|
c9bc2b48b0 | ||
|
|
b1689e0d3c | ||
|
|
c70c370ae0 | ||
|
|
c0ec5eeffd | ||
|
|
c81c2a4630 | ||
|
|
24b2e6e585 | ||
|
|
3fd1310008 | ||
|
|
17bc1a3e1a | ||
|
|
f5d9a3714d | ||
|
|
4749656828 | ||
|
|
e0bcafd89a | ||
|
|
84895c4036 | ||
|
|
a8bf9dd5b4 | ||
|
|
a61c94ef10 | ||
|
|
a8a864f70e | ||
|
|
a231dfd8e4 | ||
|
|
321d08e3d5 | ||
|
|
8c43fd8d36 | ||
|
|
9e032b6eea | ||
|
|
3e2810dff4 | ||
|
|
18afa97674 | ||
|
|
44c4aa931a | ||
|
|
95b32b168d | ||
|
|
2659f323cd | ||
|
|
1002c15e0d | ||
|
|
d51000c4a9 | ||
|
|
ba302bed33 | ||
|
|
9941d245e1 | ||
|
|
6900b4e506 | ||
|
|
6fa20f6dcf | ||
|
|
8eacb0d635 | ||
|
|
84c320d5d3 | ||
|
|
1a5a2e7a8e | ||
|
|
d87079ec13 | ||
|
|
fbd4332c87 | ||
|
|
7c4517ca3c | ||
|
|
bc4b6846ef | ||
|
|
f408fecdd0 | ||
|
|
038e86e0d8 | ||
|
|
41cee1cfe0 | ||
|
|
7e2d174cfb | ||
|
|
52da58a294 | ||
|
|
0f8caba3a5 | ||
|
|
0f8fe1f63f | ||
|
|
ed32e16db2 | ||
|
|
08464f23bc | ||
|
|
ef42abea4b | ||
|
|
4407dd12bd | ||
|
|
d546ae38c4 | ||
|
|
7a11ec6ea4 | ||
|
|
095c86cf90 | ||
|
|
632583ede2 | ||
|
|
9c925f6778 | ||
|
|
61f9d4ab3a | ||
|
|
de6b8ba4de | ||
|
|
ab7349dbc8 | ||
|
|
292004703e | ||
|
|
c3e1e46d58 | ||
|
|
aba2f35092 | ||
|
|
b3a3dfb79f | ||
|
|
470bfc3f1f | ||
|
|
ea3180cb8c | ||
|
|
72d3f8f8e2 | ||
|
|
af9e03904c | ||
|
|
39611cbd60 | ||
|
|
4e98a76c19 | ||
|
|
14767e6edb | ||
|
|
39bc8828cd | ||
|
|
c183f57670 | ||
|
|
009eb7f905 | ||
|
|
eb7a8beb77 | ||
|
|
6f0d3003ac | ||
|
|
ee17b0739a | ||
|
|
47436f7bdd | ||
|
|
3352c95314 | ||
|
|
77123ac74c | ||
|
|
9078f0ac6e | ||
|
|
8d6567d067 | ||
|
|
6d5cddca12 | ||
|
|
a3939e9a05 | ||
|
|
eee621d15b | ||
|
|
7b2ee37437 | ||
|
|
f014fb2895 | ||
|
|
1011d1ff0b | ||
|
|
9e351b279e | ||
|
|
a0bdc0821c | ||
|
|
a819d9e3e0 | ||
|
|
49f3c56680 | ||
|
|
1657b1744c | ||
|
|
a126482d69 | ||
|
|
c74b935a9b | ||
|
|
015444cfd5 | ||
|
|
a55d504017 | ||
|
|
87b5adcaf4 | ||
|
|
c14c148af7 | ||
|
|
56e46c43f8 | ||
|
|
c4e6313c90 | ||
|
|
18e46969b7 | ||
|
|
e8cc6e4205 | ||
|
|
5f0bcd2bfa | ||
|
|
a3bef76083 | ||
|
|
66563d93cc | ||
|
|
3f25a3d005 | ||
|
|
98a10d2ca1 | ||
|
|
fdc59b1ec9 | ||
|
|
1b399fc0a2 | ||
|
|
5f8c099829 | ||
|
|
599aaa4c1b | ||
|
|
1884081ebe | ||
|
|
3d425f44de | ||
|
|
d8991c5459 | ||
|
|
c7cb87dd57 | ||
|
|
1145af43e7 | ||
|
|
d5b2742fbb | ||
|
|
a0eb972595 | ||
|
|
3bf70f4315 | ||
|
|
a8130bd4f2 | ||
|
|
a35d95207e | ||
|
|
b18c8e079e | ||
|
|
4e9f563b45 | ||
|
|
7ece383004 | ||
|
|
4ca9558e30 | ||
|
|
89582d526e | ||
|
|
5913f7efab | ||
|
|
1367c5630f | ||
|
|
0ca27d8edf | ||
|
|
60c6cc35d5 | ||
|
|
6b783b3998 | ||
|
|
55515f2fc6 | ||
|
|
7fe07c65fd | ||
|
|
ee204a9b5e | ||
|
|
e743e04b94 | ||
|
|
cf8e8b94eb | ||
|
|
d9557da813 | ||
|
|
c19b983914 | ||
|
|
f997fdb232 | ||
|
|
f0b037f37f | ||
|
|
4d307e18eb | ||
|
|
e05bd8efd6 | ||
|
|
71462696bd | ||
|
|
fb79cde729 | ||
|
|
c986290d83 | ||
|
|
af9afab821 | ||
|
|
14af7b756e | ||
|
|
ff605873ed | ||
|
|
6c49c7ee0a | ||
|
|
157d1664cf | ||
|
|
6f92b7e120 | ||
|
|
fd62acd232 | ||
|
|
1cdf830217 | ||
|
|
0b48ab20bb | ||
|
|
081a2b2fa6 | ||
|
|
bf93d10cde | ||
|
|
05ef5d5f51 | ||
|
|
4cdae3ca98 | ||
|
|
7dcaeed33a | ||
|
|
f52b220d01 | ||
|
|
50a850f3a9 | ||
|
|
886ae17274 | ||
|
|
a9b6d481cc | ||
|
|
85d6daabac | ||
|
|
5f1918d005 | ||
|
|
16bd79fa1b | ||
|
|
c2ebdefe8d | ||
|
|
37032892a4 | ||
|
|
6b43438b74 | ||
|
|
7301bf89bb | ||
|
|
ff23b28e7c | ||
|
|
931e95f391 | ||
|
|
f7571b5b69 | ||
|
|
801ad18a89 | ||
|
|
67a347c0c4 | ||
|
|
693905eb68 | ||
|
|
f3e79c6b4c | ||
|
|
52f207eaa6 | ||
|
|
67294217a8 | ||
|
|
d111a40ce8 | ||
|
|
0c5bb3fcfe | ||
|
|
3fba8c20a0 | ||
|
|
54db6049b7 | ||
|
|
81c38d6b2b | ||
|
|
e6a86c02ae | ||
|
|
a0f7f32570 | ||
|
|
c39fce8839 | ||
|
|
bd2d0bc489 | ||
|
|
de37489c53 | ||
|
|
2a35a86de6 | ||
|
|
0a30a77c08 | ||
|
|
1a99bacfe3 | ||
|
|
2bf35f1651 | ||
|
|
48cc277468 | ||
|
|
d8498fd655 | ||
|
|
7ec8e33b83 | ||
|
|
8eb2d0c2a1 | ||
|
|
5995cc276d | ||
|
|
94c2ab4395 | ||
|
|
15179db945 | ||
|
|
0f07cab920 | ||
|
|
4f8b397081 | ||
|
|
2df74d9755 | ||
|
|
0b25492edc | ||
|
|
5f0a9d0b08 | ||
|
|
54f5dbbd35 | ||
|
|
95a5cde359 | ||
|
|
3d08ef43f2 | ||
|
|
381ff186cb | ||
|
|
e32fa28a6c | ||
|
|
1842e8f51c | ||
|
|
bfa51c2d87 | ||
|
|
81e781133d | ||
|
|
9ec7757efe | ||
|
|
7bd7ec4937 | ||
|
|
14d8583c83 | ||
|
|
a004d96cd7 | ||
|
|
21c6de2f80 | ||
|
|
a8e78f27e1 | ||
|
|
6c7ab6ff54 | ||
|
|
659ee6ba71 | ||
|
|
83ee8dec15 | ||
|
|
87cbbed9e3 | ||
|
|
a058520628 | ||
|
|
1ef5ebfb52 | ||
|
|
bbc06931ad | ||
|
|
2d3bf9b981 | ||
|
|
fd80f2a89f | ||
|
|
abb3dec700 | ||
|
|
80fcbceeb3 | ||
|
|
9a893a854e | ||
|
|
b0f65a02f2 | ||
|
|
9bf6d0621f | ||
|
|
fcadacb0ad | ||
|
|
8f07689c57 | ||
|
|
0630298fb9 | ||
|
|
9f61301423 | ||
|
|
9c101daad7 | ||
|
|
0586ef6d41 | ||
|
|
8fc42558d3 | ||
|
|
5d98a6964e | ||
|
|
9d8bf8ddcb | ||
|
|
4c8aad0e68 | ||
|
|
eb6f0f710c | ||
|
|
d9d2a702ef | ||
|
|
cbf5bd57a8 | ||
|
|
2bf6b04726 | ||
|
|
12a0660342 | ||
|
|
ab478ed4cf | ||
|
|
533541cf45 | ||
|
|
bab0fd3d4f | ||
|
|
375792a686 | ||
|
|
7155c788e2 | ||
|
|
3ac926b39b | ||
|
|
ea3444e2fe | ||
|
|
083c06d170 | ||
|
|
286fbba9f6 | ||
|
|
778603e1bf | ||
|
|
411d4b0779 | ||
|
|
bce0e8c37c | ||
|
|
507002d34e | ||
|
|
34a8144f51 | ||
|
|
4709a42f4f | ||
|
|
d04d9bf7f3 | ||
|
|
412f0d9c61 | ||
|
|
0ad8bf45fc | ||
|
|
3ab3ffe3ea | ||
|
|
064d5f3fa6 | ||
|
|
46626a3f98 | ||
|
|
90ba27fbab | ||
|
|
76c293e403 | ||
|
|
78e57c1a51 | ||
|
|
7c49dc6af9 | ||
|
|
2e60a088ab | ||
|
|
9ab0f825f4 | ||
|
|
411ca28598 | ||
|
|
cc2b5e63ca | ||
|
|
ea29557a33 | ||
|
|
b53f531f2b | ||
|
|
bd74ebed1e | ||
|
|
1632e9c55b | ||
|
|
6dd6a5dd80 | ||
|
|
b65b90d78c | ||
|
|
6a975994cc | ||
|
|
3ae596783d | ||
|
|
ce6e956434 | ||
|
|
f560a1f815 | ||
|
|
d14ba0c2e8 | ||
|
|
34257cfc1f | ||
|
|
8f990feabb | ||
|
|
fff2524ee2 | ||
|
|
a756e7f8f3 | ||
|
|
bc257d2d6d | ||
|
|
7275f9b8e3 | ||
|
|
b63827196c | ||
|
|
2881face70 | ||
|
|
77ba09c310 | ||
|
|
49aa0fbb9f | ||
|
|
7846b45bef | ||
|
|
7e26f3502d | ||
|
|
47a866400c | ||
|
|
4b6c26bd0b | ||
|
|
94c47383a9 | ||
|
|
6bcecc2677 | ||
|
|
e20c3520dc | ||
|
|
24fb1868d7 | ||
|
|
82afb804e1 | ||
|
|
0951546356 | ||
|
|
f09c739c1f | ||
|
|
df9ddd5451 | ||
|
|
fe513951d3 | ||
|
|
7f0d509a88 | ||
|
|
54f671b2f1 | ||
|
|
c2c89c6c86 | ||
|
|
75652d0e9f | ||
|
|
6535dc2e38 | ||
|
|
6b2f768c8f | ||
|
|
6099c3113f | ||
|
|
21d8a6952f | ||
|
|
b84f4e03ea | ||
|
|
ff21a095cb | ||
|
|
fa091a19c6 | ||
|
|
e3ba9fa5cc | ||
|
|
560f67522f | ||
|
|
11118efd84 | ||
|
|
9c2bc6affc | ||
|
|
4fe31a20e3 | ||
|
|
0f99c1ad20 | ||
|
|
91db055d86 | ||
|
|
5540149e2b | ||
|
|
7c7e351d34 | ||
|
|
c5051b343e | ||
|
|
02a0d592f9 | ||
|
|
071e9e3fec | ||
|
|
4a01303438 |
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: kiwix
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: https://kiwix.org/support-us/
|
||||
custom: # https://kiwix.org/support-us/
|
||||
|
||||
163
.github/workflows/ci.yml
vendored
Normal file
163
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
Macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v1
|
||||
- name: Setup python 3.5
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.5'
|
||||
- name: Install packages
|
||||
run: |
|
||||
brew update
|
||||
brew install gcovr pkg-config ninja
|
||||
- name: Install python modules
|
||||
run: pip3 install meson==0.49.2 pytest
|
||||
- name: Install deps
|
||||
shell: bash
|
||||
run: |
|
||||
ARCHIVE_NAME=deps2_osx_native_dyn_kiwix-lib.tar.xz
|
||||
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C $HOME
|
||||
- name: Compile
|
||||
shell: bash
|
||||
run: |
|
||||
export PKG_CONFIG_PATH=$HOME/BUILD_native_dyn/INSTALL/lib/pkgconfig
|
||||
export CPPFLAGS="-I$HOME/BUILD_native_dyn/INSTALL/include"
|
||||
meson . build --default-library=shared -Db_coverage=true
|
||||
cd build
|
||||
ninja
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
export LD_LIBRARY_PATH=$HOME/BUILD_native_dyn/INSTALL/lib:$HOME/BUILD_native_dyn/INSTALL/lib64
|
||||
cd build
|
||||
meson test --verbose
|
||||
ninja coverage
|
||||
env:
|
||||
SKIP_BIG_MEMORY_TEST: 1
|
||||
- name: Publish coverage
|
||||
shell: bash
|
||||
run: |
|
||||
curl https://codecov.io/bash -o codecov.sh
|
||||
bash codecov.sh -n osx_native_dyn -Z
|
||||
rm codecov.sh
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
Linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name:
|
||||
- native_static
|
||||
- native_dyn
|
||||
- native_dyn_bionic
|
||||
- android_arm
|
||||
- android_arm64
|
||||
- win32_static
|
||||
- win32_dyn
|
||||
include:
|
||||
- name: native_static
|
||||
target: native_static
|
||||
image_variant: xenial
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: native_dyn
|
||||
target: native_dyn
|
||||
image_variant: xenial
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: native_dyn_bionic
|
||||
target: native_dyn
|
||||
image_variant: bionic
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: android_arm
|
||||
target: android_arm
|
||||
image_variant: xenial
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: android_arm64
|
||||
target: android_arm64
|
||||
image_variant: xenial
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: win32_static
|
||||
target: win32_static
|
||||
image_variant: f31
|
||||
lib_postfix: '64'
|
||||
- name: win32_dyn
|
||||
target: win32_dyn
|
||||
image_variant: f31
|
||||
lib_postfix: '64'
|
||||
env:
|
||||
HOME: /home/runner
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: "kiwix/kiwix-build_ci:${{matrix.image_variant}}-26"
|
||||
steps:
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
|
||||
id: extract_branch
|
||||
- name: Checkout code
|
||||
shell: python
|
||||
run: |
|
||||
from subprocess import check_call
|
||||
from os import environ
|
||||
command = [
|
||||
'git', 'clone',
|
||||
'https://github.com/${{github.repository}}',
|
||||
'--depth=1',
|
||||
'--branch', '${{steps.extract_branch.outputs.branch}}'
|
||||
]
|
||||
check_call(command, cwd=environ['HOME'])
|
||||
- name: Install deps
|
||||
shell: bash
|
||||
run: |
|
||||
ARCHIVE_NAME=deps2_${OS_NAME}_${{matrix.target}}_kiwix-lib.tar.xz
|
||||
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C /home/runner
|
||||
- name: Compile
|
||||
shell: bash
|
||||
run: |
|
||||
meson --version
|
||||
if [[ "${{matrix.target}}" =~ .*_dyn ]]; then
|
||||
MESON_OPTION="--default-library=shared"
|
||||
else
|
||||
MESON_OPTION="--default-library=static"
|
||||
fi
|
||||
if [[ "${{matrix.target}}" =~ native_.* ]]; then
|
||||
MESON_OPTION="$MESON_OPTION -Db_coverage=true"
|
||||
else
|
||||
MESON_OPTION="$MESON_OPTION --cross-file $HOME/BUILD_${{matrix.target}}/meson_cross_file.txt"
|
||||
fi
|
||||
if [[ "${{matrix.target}}" =~ android_.* ]]; then
|
||||
MESON_OPTION="$MESON_OPTION -Dandroid=true"
|
||||
fi
|
||||
cd $HOME/libkiwix
|
||||
meson . build ${MESON_OPTION}
|
||||
cd build
|
||||
ninja
|
||||
env:
|
||||
PKG_CONFIG_PATH: "/home/runner/BUILD_${{matrix.target}}/INSTALL/lib/pkgconfig:/home/runner/BUILD_${{matrix.target}}/INSTALL/lib${{matrix.lib_postfix}}/pkgconfig"
|
||||
CPPFLAGS: "-I/home/runner/BUILD_${{matrix.target}}/INSTALL/include"
|
||||
- name: Test
|
||||
if: startsWith(matrix.target, 'native_')
|
||||
shell: bash
|
||||
run: |
|
||||
cd $HOME/libkiwix/build
|
||||
meson test --verbose
|
||||
ninja coverage
|
||||
env:
|
||||
LD_LIBRARY_PATH: "/home/runner/BUILD_${{matrix.target}}/INSTALL/lib:/home/runner/BUILD_${{matrix.target}}/INSTALL/lib${{matrix.lib_postfix}}"
|
||||
SKIP_BIG_MEMORY_TEST: 1
|
||||
- name: Publish coverage
|
||||
shell: bash
|
||||
run: |
|
||||
cd $HOME/libkiwix
|
||||
curl https://codecov.io/bash -o codecov.sh
|
||||
bash codecov.sh -n "${OS_NAME}_${{matrix.target}}" -Z
|
||||
rm codecov.sh
|
||||
if: startsWith(matrix.target, 'native_') && matrix.image_variant == 'xenial'
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
87
.github/workflows/package.yml
vendored
Normal file
87
.github/workflows/package.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Packages
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build-deb:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
distro: [ubuntu-hirsute, ubuntu-groovy, ubuntu-focal, ubuntu-bionic]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Determine which PPA we should upload to
|
||||
- name: PPA
|
||||
id: ppa
|
||||
run: |
|
||||
if [[ $REF == refs/tags* ]]
|
||||
then
|
||||
echo "::set-output name=ppa::kiwixteam/release"
|
||||
else
|
||||
echo "::set-output name=ppa::kiwixteam/dev"
|
||||
fi
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
|
||||
- uses: legoktm/gh-action-auto-dch@master
|
||||
with:
|
||||
fullname: Kiwix builder
|
||||
email: release+launchpad@kiwix.org
|
||||
distro: ${{ matrix.distro }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-hirsute
|
||||
if: matrix.distro == 'ubuntu-hirsute'
|
||||
name: Build package for ubuntu-hirsute
|
||||
id: build-ubuntu-hirsute
|
||||
with:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-groovy
|
||||
if: matrix.distro == 'ubuntu-groovy'
|
||||
name: Build package for ubuntu-groovy
|
||||
id: build-ubuntu-groovy
|
||||
with:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-focal
|
||||
if: matrix.distro == 'ubuntu-focal'
|
||||
name: Build package for ubuntu-focal
|
||||
id: build-ubuntu-focal
|
||||
with:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-bionic
|
||||
if: matrix.distro == 'ubuntu-bionic'
|
||||
name: Build package for ubuntu-bionic
|
||||
id: build-ubuntu-bionic
|
||||
with:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Packages for ${{ matrix.distro }}
|
||||
path: output
|
||||
|
||||
- uses: legoktm/gh-action-dput@master
|
||||
name: Upload dev package
|
||||
# Only upload on pushes to master
|
||||
if: github.event_name == 'push' && github.event.ref == 'refs/heads/master' && startswith(matrix.distro, 'ubuntu-')
|
||||
with:
|
||||
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
|
||||
repository: ppa:kiwixteam/dev
|
||||
packages: output/*_source.changes
|
||||
|
||||
- uses: legoktm/gh-action-dput@master
|
||||
name: Upload release package
|
||||
# Only upload on pushes to master or tag
|
||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && startswith(matrix.distro, 'ubuntu-')
|
||||
with:
|
||||
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
|
||||
repository: ppa:kiwixteam/release
|
||||
packages: output/*_source.changes
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
.idea/
|
||||
*.swp
|
||||
subprojects/googletest-release*
|
||||
*.class
|
||||
build/
|
||||
.vscode/
|
||||
builddir/
|
||||
|
||||
42
.travis.yml
42
.travis.yml
@@ -1,42 +0,0 @@
|
||||
language: cpp
|
||||
dist: xenial
|
||||
sudo: true
|
||||
cache: ccache
|
||||
before_install:
|
||||
- PATH=$PATH:$HOME/bin
|
||||
install: travis/install_deps.sh
|
||||
script: travis/compile.sh
|
||||
env:
|
||||
matrix:
|
||||
- PLATFORM="native_static"
|
||||
- PLATFORM="native_dyn"
|
||||
- PLATFORM="win32_static"
|
||||
- PLATFORM="win32_dyn"
|
||||
- PLATFORM="android_arm"
|
||||
- PLATFORM="android_arm64"
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- cmake
|
||||
- python3.5
|
||||
- python3-pip
|
||||
- libbz2-dev
|
||||
- ccache
|
||||
- zlib1g-dev
|
||||
- uuid-dev
|
||||
- libctpp2-dev
|
||||
- ctpp2-utils
|
||||
- libmicrohttpd-dev
|
||||
- g++-mingw-w64-i686
|
||||
- gcc-mingw-w64-i686
|
||||
- gcc-mingw-w64-base
|
||||
- mingw-w64-tools
|
||||
- gcovr
|
||||
homebrew:
|
||||
update: true
|
||||
packages:
|
||||
- gcovr
|
||||
matrix:
|
||||
include:
|
||||
- env: PLATFORM="native_dyn"
|
||||
os: osx
|
||||
124
ChangeLog
124
ChangeLog
@@ -1,3 +1,127 @@
|
||||
libkiwix 10.0.0
|
||||
===============
|
||||
|
||||
* ...
|
||||
|
||||
kiwix-lib 9.4.1
|
||||
===============
|
||||
|
||||
* Fix `M/Counter` parsing.
|
||||
* [SERVER] Adjust body padding-top for taskbar
|
||||
* Fix potential crash when stoping a server not started.
|
||||
* Various fix in build system and the CI.
|
||||
|
||||
kiwix-lib 9.4.0
|
||||
===============
|
||||
|
||||
* [SERVER] Make the headers handling case insensitive.
|
||||
* [SERVER] Make server answer 204 http status code for empty search
|
||||
* [PACKAGING] Made CI build deb packages.
|
||||
* [SERVER] Add a way to prevent taskbar and external link bloquer at article
|
||||
level.
|
||||
* Fix meson file to be compatible with meson 0.45
|
||||
* [SERVER] Update search requests to use pageStart/pageLength instead of
|
||||
pageStart/pageEnd arguments.
|
||||
* [SERVER] Set a fixed favicon size in the main page.
|
||||
* [SERVER] Refactor the response system code to better handling future new
|
||||
libzim api.
|
||||
* Fix segmentation fault around exchange with aria2 process making
|
||||
kiwix-desktop crash at exit.
|
||||
|
||||
kiwix-lib 9.3.1
|
||||
===============
|
||||
|
||||
* Fix handling of samba path on windows.
|
||||
* Do not include `kiwix_config.h` in public header.
|
||||
* Fix compilation with libmicrohttpd v0.97.1
|
||||
* Increase default test timeout to 160seconds/test.
|
||||
* Add automatic debian packaging.
|
||||
* Use non-minified version of jquery-ui.js
|
||||
* Pass `-latomic` compile option for sh4 architecture.
|
||||
* Make mesion install `kiwix-compile-resources` man page.
|
||||
|
||||
kiwix-lib 9.3.0
|
||||
===============
|
||||
|
||||
* Add a thread safe method to search suggestions.
|
||||
Old methods are now deprecated.
|
||||
|
||||
kiwix-lib 9.2.3
|
||||
===============
|
||||
|
||||
* Add test on byte-range
|
||||
* Fix compilation on bionic and windows.
|
||||
* Allow building using debian packaged kainjow-mustache
|
||||
* Pass `-latomic` compile option for architectures that need it
|
||||
|
||||
kiwix-lib 9.2.2
|
||||
===============
|
||||
|
||||
* Fix handling on empty content in byte range management (wrong assert)
|
||||
|
||||
kiwix-lib 9.2.1
|
||||
===============
|
||||
|
||||
* Fix support of byte range request.
|
||||
|
||||
kiwix-lib 9.2
|
||||
=============
|
||||
|
||||
* Add tests
|
||||
* Refactoring server code.
|
||||
* [SERVER] Add HEAD, Etag and If-None-Match support.
|
||||
* [SERVER] Compress opds catalog answers.
|
||||
|
||||
kiwix-lib 9.1.2
|
||||
===============
|
||||
|
||||
* Do not use the pathToSave if it is empty.
|
||||
|
||||
kiwix-lib 9.1.1
|
||||
===============
|
||||
|
||||
* Fix the detection of the dataDirectory on windows.
|
||||
|
||||
kiwix-lib 9.1.0
|
||||
===============
|
||||
|
||||
* [JAVA] Add a method to get the size of an article.
|
||||
* Add a new method to the libray to get the book by path.
|
||||
* Add a option to make the server blocks external link.
|
||||
Links are intercepted by js an redirected to a "portail" page.
|
||||
* [ODPS] Correctly handle book's articleCount and mediaCount.
|
||||
|
||||
kiwix-lib 9.0.1
|
||||
===============
|
||||
|
||||
* [JAVA] Use a long to store the offset of an article in the zim file instead
|
||||
of an int.
|
||||
|
||||
kiwix-lib 9.0.0
|
||||
===============
|
||||
|
||||
* [OPDS] Correctly set the id of the OPDS stream.
|
||||
* [OPDS] Do not try to filter the catalog if no filter field is given in the
|
||||
request.
|
||||
* [WINDOWS] Correctly convert path to wide chars when opening the library.xml
|
||||
* [LIBRARY] Remove the function the read file using a native path.
|
||||
All path must be utf8, no need to pass a native path along the utf8 path.
|
||||
* [TEST] Fix tests using the main function of gtest instead of custom one.
|
||||
* [CI] Move to github CI instead of Travis.
|
||||
* The `Book::update` method always update the book's fields. Even if they are
|
||||
not empty.
|
||||
* [JAVA] Add wrapping around the library manager (opds parsing)
|
||||
* [ARIA2] Add api option to start download with option (destination folder)
|
||||
* [OPDS] Fixes about opds parsing, generation (missing attributes)
|
||||
and requesting (server)
|
||||
* Add methods on `Book` to get specific tag values (was on `Reader` only)
|
||||
* Add flavour attribute to `Book`
|
||||
* Fix opensearch description.
|
||||
* Trust the given library.xml (by default) instead of reading the value from
|
||||
the zim files.
|
||||
* [OPDS] Be able to filter the content by name or size.
|
||||
* [WINDOWS] Fix launching subcommand when there is spaces in the path.
|
||||
|
||||
kiwix-lib 8.2.2
|
||||
===============
|
||||
|
||||
|
||||
73
README.md
73
README.md
@@ -1,15 +1,14 @@
|
||||
Kiwix library
|
||||
=============
|
||||
Libkiwix
|
||||
========
|
||||
|
||||
The Kiwix library provides the [Kiwix](https://kiwix.org) software
|
||||
suite core. It contains the code shared by all Kiwix ports (Windows,
|
||||
The Libkiwix provides the [Kiwix](https://kiwix.org) software suite
|
||||
core. It contains the code shared by all Kiwix ports (Windows,
|
||||
GNU/Linux, macOS, Android, iOS, ...).
|
||||
|
||||
[](https://bintray.com/kiwix/kiwix/kiwixlib/_latestVersion)
|
||||
[](https://aur.archlinux.org/packages/kiwix-lib/)
|
||||
[](https://travis-ci.com/kiwix/kiwix-lib)
|
||||
[](https://www.codefactor.io/repository/github/kiwix/kiwix-lib)
|
||||
[](https://codecov.io/gh/kiwix/kiwix-lib)
|
||||
[](https://github.com/kiwix/libkiwix/wiki/Repology)
|
||||
[](https://github.com/kiwix/libkiwix/actions?query=branch%3Amaster)
|
||||
[](https://www.codefactor.io/repository/github/kiwix/libkiwix)
|
||||
[](https://codecov.io/gh/kiwix/libkiwix)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
Disclaimer
|
||||
@@ -17,31 +16,38 @@ Disclaimer
|
||||
|
||||
This document assumes you have a little knowledge about software
|
||||
compilation. If you experience difficulties with the dependencies or
|
||||
with the Kiwix libary compilation itself, we recommend to have a look
|
||||
to [kiwix-build](https://github.com/kiwix/kiwix-build).
|
||||
with the Libkiwix compilation itself, we recommend to have a look to
|
||||
[kiwix-build](https://github.com/kiwix/kiwix-build).
|
||||
|
||||
Preamble
|
||||
--------
|
||||
|
||||
Although the Kiwix library can be (cross-)compiled on/for many
|
||||
sytems, the following documentation explains how to do it on POSIX
|
||||
ones. It is primarly thought for GNU/Linux systems and has been tested
|
||||
on recent releases of Ubuntu and Fedora.
|
||||
Although the Libkiwix can be (cross-)compiled on/for many sytems, the
|
||||
following documentation explains how to do it on POSIX ones. It is
|
||||
primarly thought for GNU/Linux systems and has been tested on recent
|
||||
releases of Ubuntu and Fedora.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
The Kiwix library relies on many third parts software libraries. They
|
||||
are prerequisites to the Kiwix library compilation. Following
|
||||
libraries need to be available:
|
||||
|
||||
The Libkiwix relies on many third party software libraries. They are
|
||||
prerequisites to the Libkiwix compilation. Following libraries need to
|
||||
be available:
|
||||
* [ICU](https://site.icu-project.org/) (package `libicu-dev` on Ubuntu)
|
||||
* [ZIM](https://openzim.org/) (package `libzim-dev` on Ubuntu)
|
||||
* [Pugixml](https://pugixml.org/) (package `libpugixml-dev` on Ubuntu)
|
||||
* [Aria2](https://aria2.github.io/) (package `aria2` on Ubuntu)
|
||||
* [Mustache](https://github.com/kainjow/Mustache) (Just copy the
|
||||
header `mustache.hpp` somewhere it can be found by the compiler and/or
|
||||
set CPPFLAGS with correct `-I` option)
|
||||
set CPPFLAGS with correct `-I` option). Use Mustache version 4.1 or above.
|
||||
* [Libcurl](https://curl.se/libcurl) (`libcurl4-gnutls-dev`, `libcurl4-nss-dev` or `libcurl4-openssl-dev` on Ubuntu)
|
||||
* [Microhttpd](https://www.gnu.org/software/libmicrohttpd) (package `libmicrohttpd-dev` on Ubuntu)
|
||||
* [Zlib](https://zlib.net/) (package `zlib1g-dev` on Ubuntu)
|
||||
|
||||
To test the code:
|
||||
* [Google Test](https://github.com/google/googletest) (package `googletest` on Ubuntu)
|
||||
|
||||
The following dependency needs to be available at runtime:
|
||||
* [Aria2](https://aria2.github.io/) (package `aria2` on Ubuntu)
|
||||
|
||||
These dependencies may or may not be packaged by your operating
|
||||
system. They may also be packaged but only in an older version. The
|
||||
@@ -50,13 +56,13 @@ In the worse case, you will have to download and compile bleeding edge
|
||||
version by hand.
|
||||
|
||||
If you want to install these dependencies locally, then use the
|
||||
`kiwix-lib` directory as install prefix.
|
||||
`libkiwix` directory as install prefix.
|
||||
|
||||
Environment
|
||||
-------------
|
||||
|
||||
The Kiwix library builds using [Meson](https://mesonbuild.com/) version
|
||||
0.43 or higher. Meson relies itself on Ninja, pkg-config and few other
|
||||
The Libkiwix builds using [Meson](https://mesonbuild.com/) version
|
||||
0.45 or higher. Meson relies itself on Ninja, pkg-config and few other
|
||||
compilation tools.
|
||||
|
||||
Install first the few common compilation tools:
|
||||
@@ -71,7 +77,7 @@ section.
|
||||
Compilation
|
||||
-----------
|
||||
|
||||
Once all dependencies are installed, you can compile the Kiwix library
|
||||
Once all dependencies are installed, you can compile the Libkiwix
|
||||
with:
|
||||
```bash
|
||||
meson . build
|
||||
@@ -79,16 +85,25 @@ ninja -C build
|
||||
```
|
||||
|
||||
By default, it will compile dynamic linked libraries. All binary files
|
||||
will be created in the "build" directory created automatically by
|
||||
will be created in the `build` directory created automatically by
|
||||
Meson. If you want statically linked libraries, you can add
|
||||
`--default-library=static` option to the Meson command.
|
||||
|
||||
Depending of you system, `ninja` may be called `ninja-build`.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
To run the automated tests:
|
||||
```bash
|
||||
cd build
|
||||
meson test
|
||||
```
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
If you want to install the Kiwix library and the headers you just have
|
||||
If you want to install the Libkiwix and the headers you just have
|
||||
compiled on your system, here we go:
|
||||
```bash
|
||||
ninja -C build install
|
||||
@@ -131,6 +146,10 @@ cp ninja ../bin
|
||||
cd ..
|
||||
```
|
||||
|
||||
If you compile manually Libmicrohttpd, you might need to compile it
|
||||
without GNU TLS, a bug here will empeach further compilation
|
||||
otherwise.
|
||||
|
||||
If the compilation still fails, you might need to get a more recent
|
||||
version of a dependency than the one packaged by your Linux
|
||||
distribution. Try then with a source tarball distributed by the
|
||||
|
||||
@@ -26,10 +26,10 @@ task writePom {
|
||||
project {
|
||||
groupId 'org.kiwix.kiwixlib'
|
||||
artifactId 'kiwixlib'
|
||||
version '8.2.2' + (System.env.KIWIXLIB_BUILDVERSION == null ? '' : '-'+System.env.KIWIXLIB_BUILDVERSION)
|
||||
version '10.0.0' + (System.env.KIWIXLIB_BUILDVERSION == null ? '' : '-'+System.env.KIWIXLIB_BUILDVERSION)
|
||||
packaging 'aar'
|
||||
name 'kiwixlib'
|
||||
url 'https://github.com/kiwix/kiwix-lib'
|
||||
url 'https://github.com/kiwix/libkiwix'
|
||||
licenses {
|
||||
license {
|
||||
name 'GPLv3'
|
||||
@@ -44,9 +44,9 @@ task writePom {
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection 'https://github.com/kiwix/kiwix-lib.git'
|
||||
developerConnection 'https://github.com/kiwix/kiwix-lib.git'
|
||||
url 'https://github.com/kiwix/kiwix-lib'
|
||||
connection 'https://github.com/kiwix/libkiwix.git'
|
||||
developerConnection 'https://github.com/kiwix/libkiwix.git'
|
||||
url 'https://github.com/kiwix/libkiwix'
|
||||
}
|
||||
}
|
||||
}.withXml {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="kiwix.org.kiwixlib">
|
||||
package="org.kiwix.kiwixlib">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
||||
5
debian/changelog
vendored
Normal file
5
debian/changelog
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
libkiwix (0.0.0) unstable; urgency=medium
|
||||
|
||||
* Initial release
|
||||
|
||||
-- Kunal Mehta <legoktm@debian.org> Wed, 08 Jul 2020 18:12:32 -0700
|
||||
46
debian/control
vendored
Normal file
46
debian/control
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
Source: libkiwix
|
||||
Priority: optional
|
||||
Maintainer: Kiwix team <kiwix@kiwix.org>
|
||||
Build-Depends: debhelper-compat (= 13),
|
||||
meson,
|
||||
pkg-config,
|
||||
libzim-dev (>= 6.1.8),
|
||||
libcurl4-gnutls-dev,
|
||||
libicu-dev,
|
||||
libgtest-dev,
|
||||
libkainjow-mustache-dev,
|
||||
liblzma-dev,
|
||||
libmicrohttpd-dev,
|
||||
libpugixml-dev,
|
||||
zlib1g-dev
|
||||
Standards-Version: 4.5.0
|
||||
Section: libs
|
||||
Homepage: https://github.com/kiwix/libkiwix
|
||||
Rules-Requires-Root: no
|
||||
|
||||
Package: libkiwix-dev
|
||||
Section: libdevel
|
||||
Architecture: any
|
||||
Multi-Arch: same
|
||||
Depends: libkiwix10 (= ${binary:Version}), ${misc:Depends}, python3,
|
||||
libzim-dev (>= 6.0.0),
|
||||
libicu-dev,
|
||||
libpugixml-dev,
|
||||
libcurl4-gnutls-dev,
|
||||
libmicrohttpd-dev
|
||||
Description: library of common code for Kiwix (development)
|
||||
Kiwix is an offline Wikipedia reader. libkiwix provides the
|
||||
software core for Kiwix, and contains the code shared by all
|
||||
Kiwix ports (Windows, Linux, OSX, Android, etc.).
|
||||
.
|
||||
This package contains development files.
|
||||
|
||||
Package: libkiwix10
|
||||
Architecture: any
|
||||
Multi-Arch: same
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}, aria2
|
||||
Conflicts: libkiwix0, libkiwix3, libkiwix9
|
||||
Description: library of common code for Kiwix
|
||||
Kiwix is an offline Wikipedia reader. libkiwix provides the
|
||||
software core for Kiwix, and contains the code shared by all
|
||||
Kiwix ports (Windows, Linux, OSX, Android, etc.).
|
||||
1
debian/copyright
vendored
Normal file
1
debian/copyright
vendored
Normal file
@@ -0,0 +1 @@
|
||||
See COPYING in the repository root.
|
||||
4
debian/libkiwix-dev.install
vendored
Normal file
4
debian/libkiwix-dev.install
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
usr/include
|
||||
usr/lib/*/libkiwix.so
|
||||
usr/lib/*/pkgconfig
|
||||
usr/bin
|
||||
1
debian/libkiwix-dev.manpages
vendored
Normal file
1
debian/libkiwix-dev.manpages
vendored
Normal file
@@ -0,0 +1 @@
|
||||
usr/share/man/man1/kiwix-compile-resources.1*
|
||||
1
debian/libkiwix10.install
vendored
Normal file
1
debian/libkiwix10.install
vendored
Normal file
@@ -0,0 +1 @@
|
||||
usr/lib/*/libkiwix.so.*
|
||||
8
debian/rules
vendored
Executable file
8
debian/rules
vendored
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/make -f
|
||||
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
|
||||
|
||||
%:
|
||||
dh $@ --buildsystem=meson
|
||||
|
||||
override_dh_auto_test:
|
||||
dh_auto_test -- -t 3
|
||||
1
debian/source/format
vendored
Normal file
1
debian/source/format
vendored
Normal file
@@ -0,0 +1 @@
|
||||
3.0 (native)
|
||||
@@ -59,7 +59,11 @@ class Book
|
||||
const std::string& getDate() const { return m_date; }
|
||||
const std::string& getUrl() const { return m_url; }
|
||||
const std::string& getName() const { return m_name; }
|
||||
std::string getCategory() const;
|
||||
const std::string& getTags() const { return m_tags; }
|
||||
std::string getTagStr(const std::string& tagName) const;
|
||||
bool getTagBool(const std::string& tagName) const;
|
||||
const std::string& getFlavour() const { return m_flavour; }
|
||||
const std::string& getOrigId() const { return m_origId; }
|
||||
const uint64_t& getArticleCount() const { return m_articleCount; }
|
||||
const uint64_t& getMediaCount() const { return m_mediaCount; }
|
||||
@@ -81,6 +85,7 @@ class Book
|
||||
void setDate(const std::string& date) { m_date = date; }
|
||||
void setUrl(const std::string& url) { m_url = url; }
|
||||
void setName(const std::string& name) { m_name = name; }
|
||||
void setFlavour(const std::string& flavour) { m_flavour = flavour; }
|
||||
void setTags(const std::string& tags) { m_tags = tags; }
|
||||
void setOrigId(const std::string& origId) { m_origId = origId; }
|
||||
void setArticleCount(uint64_t articleCount) { m_articleCount = articleCount; }
|
||||
@@ -90,25 +95,30 @@ class Book
|
||||
void setFaviconMimeType(const std::string& faviconMimeType) { m_faviconMimeType = faviconMimeType; }
|
||||
void setDownloadId(const std::string& downloadId) { m_downloadId = downloadId; }
|
||||
|
||||
private:
|
||||
std::string getCategoryFromTags() const;
|
||||
|
||||
protected:
|
||||
std::string m_id;
|
||||
std::string m_downloadId;
|
||||
std::string m_path;
|
||||
bool m_pathValid;
|
||||
bool m_pathValid = false;
|
||||
std::string m_title;
|
||||
std::string m_description;
|
||||
std::string m_category;
|
||||
std::string m_language;
|
||||
std::string m_creator;
|
||||
std::string m_publisher;
|
||||
std::string m_date;
|
||||
std::string m_url;
|
||||
std::string m_name;
|
||||
std::string m_flavour;
|
||||
std::string m_tags;
|
||||
std::string m_origId;
|
||||
uint64_t m_articleCount;
|
||||
uint64_t m_mediaCount;
|
||||
bool m_readOnly;
|
||||
uint64_t m_size;
|
||||
uint64_t m_articleCount = 0;
|
||||
uint64_t m_mediaCount = 0;
|
||||
bool m_readOnly = false;
|
||||
uint64_t m_size = 0;
|
||||
mutable std::string m_favicon;
|
||||
std::string m_faviconUrl;
|
||||
std::string m_faviconMimeType;
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <pthread.h>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
@@ -92,12 +92,11 @@ class Downloader
|
||||
|
||||
void close();
|
||||
|
||||
Download* startDownload(const std::string& uri);
|
||||
Download* startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
|
||||
Download* getDownload(const std::string& did);
|
||||
|
||||
size_t getNbDownload() { return m_knownDownloads.size(); }
|
||||
std::vector<std::string> getDownloadIds();
|
||||
const std::string &getAria2LaunchCmd();
|
||||
|
||||
private:
|
||||
std::map<std::string, std::unique_ptr<Download>> m_knownDownloads;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
* Copyright 2018-2020 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,7 +21,8 @@
|
||||
#define KIWIX_ENTRY_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <zim/article.h>
|
||||
#include <zim/entry.h>
|
||||
#include <zim/item.h>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
@@ -41,19 +42,12 @@ class NoEntry : public std::exception {};
|
||||
class Entry
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Default constructor.
|
||||
*
|
||||
* Construct an invalid entry.
|
||||
*/
|
||||
Entry() = default;
|
||||
|
||||
/**
|
||||
* Construct an entry making reference to an zim article.
|
||||
*
|
||||
* @param article a zim::Article object
|
||||
*/
|
||||
Entry(zim::Article article);
|
||||
Entry(zim::Entry entry);
|
||||
virtual ~Entry() = default;
|
||||
|
||||
/**
|
||||
@@ -63,14 +57,14 @@ class Entry
|
||||
*
|
||||
* @return the path of the entry.
|
||||
*/
|
||||
std::string getPath() const;
|
||||
std::string getPath() const { return entry.getPath(); }
|
||||
|
||||
/**
|
||||
* Get the title of the entry.
|
||||
*
|
||||
* @return the title of the entry.
|
||||
*/
|
||||
std::string getTitle() const;
|
||||
std::string getTitle() const { return entry.getTitle(); }
|
||||
|
||||
/**
|
||||
* Get the content of the entry.
|
||||
@@ -80,7 +74,7 @@ class Entry
|
||||
*
|
||||
* @return the content of the entry.
|
||||
*/
|
||||
std::string getContent() const;
|
||||
std::string getContent() const { return entry.getItem().getData(); }
|
||||
|
||||
/**
|
||||
* Get the blob of the entry.
|
||||
@@ -90,7 +84,7 @@ class Entry
|
||||
* @param offset The starting offset of the blob.
|
||||
* @return the blob of the entry.
|
||||
*/
|
||||
zim::Blob getBlob(offset_type offset = 0) const;
|
||||
zim::Blob getBlob(offset_type offset = 0) const { return entry.getItem().getData(offset); }
|
||||
|
||||
/**
|
||||
* Get the blob of the entry.
|
||||
@@ -101,7 +95,7 @@ class Entry
|
||||
* @param size The size of the blob.
|
||||
* @return the blob of the entry.
|
||||
*/
|
||||
zim::Blob getBlob(offset_type offset, size_type size) const;
|
||||
zim::Blob getBlob(offset_type offset, size_type size) const { return entry.getItem().getData(offset, size); }
|
||||
|
||||
/**
|
||||
* Get the info for direct access to the content of the entry.
|
||||
@@ -109,7 +103,7 @@ class Entry
|
||||
* Some entry (ie binary ones) have their content plain stored
|
||||
* in the zim file. Knowing the offset where the content is stored
|
||||
* an user can directly read the content in the zim file bypassing the
|
||||
* kiwix-lib/libzim.
|
||||
* libkiwix/libzim.
|
||||
*
|
||||
* @return A pair specifying where to read the content.
|
||||
* The string is the real file to read (may be different that .zim
|
||||
@@ -117,7 +111,7 @@ class Entry
|
||||
* The offset is the offset to read in the file.
|
||||
* Return <"",0> if is not possible to read directly.
|
||||
*/
|
||||
std::pair<std::string, offset_type> getDirectAccessInfo() const;
|
||||
zim::Item::DirectAccessInfo getDirectAccessInfo() const { return entry.getItem().getDirectAccessInformation(); }
|
||||
|
||||
/**
|
||||
* Get the size of the entry.
|
||||
@@ -174,17 +168,14 @@ class Entry
|
||||
Entry getFinalEntry() const;
|
||||
|
||||
/**
|
||||
* Convert the entry to a boolean value.
|
||||
* Get the zim entry wrapped by this (kiwix) entry
|
||||
*
|
||||
* @return True if the entry is valid.
|
||||
* @return the zim entry
|
||||
*/
|
||||
explicit operator bool() const { return good(); }
|
||||
const zim::Entry& getZimEntry() const { return entry; }
|
||||
|
||||
private:
|
||||
zim::Article article;
|
||||
mutable zim::Article final_article;
|
||||
|
||||
bool good() const { return article.good(); }
|
||||
zim::Entry entry;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace kiwix
|
||||
{
|
||||
|
||||
class OPDSDumper;
|
||||
class Library;
|
||||
|
||||
enum supportedListSortBy { UNSORTED, TITLE, SIZE, DATE, CREATOR, PUBLISHER };
|
||||
enum supportedListMode {
|
||||
@@ -48,17 +49,23 @@ enum supportedListMode {
|
||||
};
|
||||
|
||||
class Filter {
|
||||
private:
|
||||
public: // types
|
||||
using Tags = std::vector<std::string>;
|
||||
|
||||
private: // data
|
||||
uint64_t activeFilters;
|
||||
std::vector<std::string> _acceptTags;
|
||||
std::vector<std::string> _rejectTags;
|
||||
Tags _acceptTags;
|
||||
Tags _rejectTags;
|
||||
std::string _category;
|
||||
std::string _lang;
|
||||
std::string _publisher;
|
||||
std::string _creator;
|
||||
size_t _maxSize;
|
||||
std::string _query;
|
||||
bool _queryIsPartial;
|
||||
std::string _name;
|
||||
|
||||
public:
|
||||
public: // functions
|
||||
Filter();
|
||||
~Filter() = default;
|
||||
|
||||
@@ -92,14 +99,41 @@ class Filter {
|
||||
/**
|
||||
* Set the filter to only accept book with corresponding tag.
|
||||
*/
|
||||
Filter& acceptTags(std::vector<std::string> tags);
|
||||
Filter& rejectTags(std::vector<std::string> tags);
|
||||
Filter& acceptTags(const Tags& tags);
|
||||
Filter& rejectTags(const Tags& tags);
|
||||
|
||||
Filter& category(std::string category);
|
||||
Filter& lang(std::string lang);
|
||||
Filter& publisher(std::string publisher);
|
||||
Filter& creator(std::string creator);
|
||||
Filter& maxSize(size_t size);
|
||||
Filter& query(std::string query);
|
||||
Filter& query(std::string query, bool partial=true);
|
||||
Filter& name(std::string name);
|
||||
|
||||
bool hasQuery() const;
|
||||
const std::string& getQuery() const { return _query; }
|
||||
bool queryIsPartial() const { return _queryIsPartial; }
|
||||
|
||||
bool hasName() const;
|
||||
const std::string& getName() const { return _name; }
|
||||
|
||||
bool hasCategory() const;
|
||||
const std::string& getCategory() const { return _category; }
|
||||
|
||||
bool hasLang() const;
|
||||
const std::string& getLang() const { return _lang; }
|
||||
|
||||
bool hasPublisher() const;
|
||||
const std::string& getPublisher() const { return _publisher; }
|
||||
|
||||
bool hasCreator() const;
|
||||
const std::string& getCreator() const { return _creator; }
|
||||
|
||||
const Tags& getAcceptTags() const { return _acceptTags; }
|
||||
const Tags& getRejectTags() const { return _rejectTags; }
|
||||
|
||||
private: // functions
|
||||
friend class Library;
|
||||
|
||||
bool accept(const Book& book) const;
|
||||
};
|
||||
@@ -113,11 +147,24 @@ class Library
|
||||
std::map<std::string, kiwix::Book> m_books;
|
||||
std::map<std::string, std::shared_ptr<Reader>> m_readers;
|
||||
std::vector<kiwix::Bookmark> m_bookmarks;
|
||||
class BookDB;
|
||||
std::unique_ptr<BookDB> m_bookDB;
|
||||
|
||||
public:
|
||||
typedef std::vector<std::string> BookIdCollection;
|
||||
|
||||
public:
|
||||
Library();
|
||||
~Library();
|
||||
|
||||
/**
|
||||
* Library is not a copiable object. However it can be moved.
|
||||
*/
|
||||
Library(const Library& ) = delete;
|
||||
Library(Library&& );
|
||||
void operator=(const Library& ) = delete;
|
||||
Library& operator=(Library&& );
|
||||
|
||||
/**
|
||||
* Add a book to the library.
|
||||
*
|
||||
@@ -146,7 +193,10 @@ class Library
|
||||
*/
|
||||
bool removeBookmark(const std::string& zimId, const std::string& url);
|
||||
|
||||
const Book& getBookById(const std::string& id) const;
|
||||
Book& getBookById(const std::string& id);
|
||||
const Book& getBookByPath(const std::string& path) const;
|
||||
Book& getBookByPath(const std::string& path);
|
||||
std::shared_ptr<Reader> getReaderById(const std::string& id);
|
||||
|
||||
/**
|
||||
@@ -163,7 +213,7 @@ class Library
|
||||
* @param path the path of the file to write to.
|
||||
* @return True if the library has been correctly saved.
|
||||
*/
|
||||
bool writeToFile(const std::string& path);
|
||||
bool writeToFile(const std::string& path) const;
|
||||
|
||||
/**
|
||||
* Write the library bookmarks to a file.
|
||||
@@ -171,7 +221,7 @@ class Library
|
||||
* @param path the path of the file to write to.
|
||||
* @return True if the library has been correctly saved.
|
||||
*/
|
||||
bool writeBookmarksToFile(const std::string& path);
|
||||
bool writeBookmarksToFile(const std::string& path) const;
|
||||
|
||||
/**
|
||||
* Get the number of book in the library.
|
||||
@@ -180,42 +230,49 @@ class Library
|
||||
* @param remoteBooks If we must count remote books (books with an url)
|
||||
* @return The number of books.
|
||||
*/
|
||||
unsigned int getBookCount(const bool localBooks, const bool remoteBooks);
|
||||
unsigned int getBookCount(const bool localBooks, const bool remoteBooks) const;
|
||||
|
||||
/**
|
||||
* Get all langagues of the books in the library.
|
||||
* Get all languagues of the books in the library.
|
||||
*
|
||||
* @return A list of languages.
|
||||
*/
|
||||
std::vector<std::string> getBooksLanguages();
|
||||
std::vector<std::string> getBooksLanguages() const;
|
||||
|
||||
/**
|
||||
* Get all categories of the books in the library.
|
||||
*
|
||||
* @return A list of categories.
|
||||
*/
|
||||
std::vector<std::string> getBooksCategories() const;
|
||||
|
||||
/**
|
||||
* Get all book creators of the books in the library.
|
||||
*
|
||||
* @return A list of book creators.
|
||||
*/
|
||||
std::vector<std::string> getBooksCreators();
|
||||
std::vector<std::string> getBooksCreators() const;
|
||||
|
||||
/**
|
||||
* Get all book publishers of the books in the library.
|
||||
*
|
||||
* @return A list of book publishers.
|
||||
*/
|
||||
std::vector<std::string> getBooksPublishers();
|
||||
std::vector<std::string> getBooksPublishers() const;
|
||||
|
||||
/**
|
||||
* Get all bookmarks.
|
||||
*
|
||||
* @return A list of bookmarks
|
||||
*/
|
||||
const std::vector<kiwix::Bookmark> getBookmarks(bool onlyValidBookmarks = true);
|
||||
const std::vector<kiwix::Bookmark> getBookmarks(bool onlyValidBookmarks = true) const;
|
||||
|
||||
/**
|
||||
* Get all book ids of the books in the library.
|
||||
*
|
||||
* @return A list of book ids.
|
||||
*/
|
||||
std::vector<std::string> getBooksIds();
|
||||
BookIdCollection getBooksIds() const;
|
||||
|
||||
/**
|
||||
* Filter the library and generate a new one with the keep elements.
|
||||
@@ -225,7 +282,7 @@ class Library
|
||||
* @param search List only books with search in the title or description.
|
||||
* @return The list of bookIds corresponding to the query.
|
||||
*/
|
||||
DEPRECATED std::vector<std::string> filter(const std::string& search);
|
||||
DEPRECATED BookIdCollection filter(const std::string& search) const;
|
||||
|
||||
|
||||
/**
|
||||
@@ -234,7 +291,7 @@ class Library
|
||||
* @param filter The filter to use.
|
||||
* @return The list of bookIds corresponding to the filter.
|
||||
*/
|
||||
std::vector<std::string> filter(const Filter& filter);
|
||||
BookIdCollection filter(const Filter& filter) const;
|
||||
|
||||
|
||||
/**
|
||||
@@ -244,7 +301,7 @@ class Library
|
||||
* @param comparator how to sort the books
|
||||
* @return The sorted list of books
|
||||
*/
|
||||
void sort(std::vector<std::string>& bookIds, supportedListSortBy sortBy, bool ascending);
|
||||
void sort(BookIdCollection& bookIds, supportedListSortBy sortBy, bool ascending) const;
|
||||
|
||||
/**
|
||||
* List books in the library.
|
||||
@@ -268,7 +325,7 @@ class Library
|
||||
* Set to 0 to cancel this filter.
|
||||
* @return The list of bookIds corresponding to the query.
|
||||
*/
|
||||
DEPRECATED std::vector<std::string> listBooksIds(
|
||||
DEPRECATED BookIdCollection listBooksIds(
|
||||
int supportedListMode = ALL,
|
||||
supportedListSortBy sortBy = UNSORTED,
|
||||
const std::string& search = "",
|
||||
@@ -276,11 +333,16 @@ class Library
|
||||
const std::string& creator = "",
|
||||
const std::string& publisher = "",
|
||||
const std::vector<std::string>& tags = {},
|
||||
size_t maxSize = 0);
|
||||
size_t maxSize = 0) const;
|
||||
|
||||
friend class OPDSDumper;
|
||||
friend class libXMLDumper;
|
||||
|
||||
private: // functions
|
||||
BookIdCollection filterViaBookDB(const Filter& filter) const;
|
||||
void updateBookDB(const Book& book);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -38,7 +38,7 @@ class LibXMLDumper
|
||||
{
|
||||
public:
|
||||
LibXMLDumper() = default;
|
||||
LibXMLDumper(Library* library);
|
||||
LibXMLDumper(const Library* library);
|
||||
~LibXMLDumper();
|
||||
|
||||
/**
|
||||
@@ -69,10 +69,10 @@ class LibXMLDumper
|
||||
*
|
||||
* @param library The library to dump.
|
||||
*/
|
||||
void setLibrary(Library* library) { this->library = library; }
|
||||
void setLibrary(const Library* library) { this->library = library; }
|
||||
|
||||
protected:
|
||||
kiwix::Library* library;
|
||||
const kiwix::Library* library;
|
||||
std::string baseDir;
|
||||
private:
|
||||
void handleBook(Book book, pugi::xml_node root_node);
|
||||
|
||||
@@ -69,26 +69,12 @@ class Manager
|
||||
/**
|
||||
* Read a `library.xml` and add book in the file to the library.
|
||||
*
|
||||
* @param path The path to the `library.xml`.
|
||||
* @param path The (utf8) path to the `library.xml`.
|
||||
* @param readOnly Set if the libray path could be overwritten latter with
|
||||
* updated content.
|
||||
* @return True if file has been properly parsed.
|
||||
*/
|
||||
bool readFile(const std::string& path, const bool readOnly = true);
|
||||
|
||||
/**
|
||||
* Read a `library.xml` and add book in the file to the library.
|
||||
*
|
||||
* @param nativePath The path of the `library.xml`
|
||||
* @param UTF8Path The utf8 version (?) of the path. Also the path where the
|
||||
* library will be writen i readOnly is False.
|
||||
* @param readOnly Set if the libray path could be overwritten latter with
|
||||
* updated content.
|
||||
* @return True if file has been properly parsed.
|
||||
*/
|
||||
bool readFile(const std::string& nativePath,
|
||||
const std::string& UTF8Path,
|
||||
const bool readOnly = true);
|
||||
bool readFile(const std::string& path, bool readOnly = true, bool trustLibrary = true);
|
||||
|
||||
/**
|
||||
* Load a library content store in the string.
|
||||
@@ -101,7 +87,8 @@ class Manager
|
||||
*/
|
||||
bool readXml(const std::string& xml,
|
||||
const bool readOnly = true,
|
||||
const std::string& libraryPath = "");
|
||||
const std::string& libraryPath = "",
|
||||
bool trustLibrary = true);
|
||||
|
||||
/**
|
||||
* Load a library content stored in a OPDS stream.
|
||||
@@ -168,8 +155,9 @@ class Manager
|
||||
|
||||
bool readBookFromPath(const std::string& path, Book* book);
|
||||
bool parseXmlDom(const pugi::xml_document& doc,
|
||||
const bool readOnly,
|
||||
const std::string& libraryPath);
|
||||
bool readOnly,
|
||||
const std::string& libraryPath,
|
||||
bool trustLibrary);
|
||||
bool parseOpdsDom(const pugi::xml_document& doc,
|
||||
const std::string& urlHost);
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ install_headers(
|
||||
'tools/pathTools.h',
|
||||
'tools/regexTools.h',
|
||||
'tools/stringTools.h',
|
||||
'tools/lock.h',
|
||||
subdir:'kiwix/tools'
|
||||
)
|
||||
|
||||
|
||||
@@ -51,24 +51,35 @@ class OPDSDumper
|
||||
/**
|
||||
* Dump the OPDS feed.
|
||||
*
|
||||
* @param id The id of the library.
|
||||
* @param bookIds the ids of the books to include in the feed
|
||||
* @param query the query used to obtain the list of book ids
|
||||
* @return The OPDS feed.
|
||||
*/
|
||||
std::string dumpOPDSFeed(const std::vector<std::string>& bookIds);
|
||||
std::string dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const;
|
||||
|
||||
/**
|
||||
* Set the id of the opds stream.
|
||||
* Dump the OPDS feed.
|
||||
*
|
||||
* @param bookIds the ids of the books to include in the feed
|
||||
* @param query the query used to obtain the list of book ids
|
||||
* @return The OPDS feed.
|
||||
*/
|
||||
std::string dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query) const;
|
||||
|
||||
/**
|
||||
* Dump the categories OPDS feed.
|
||||
*
|
||||
* @param categories list of category names
|
||||
* @return The OPDS feed.
|
||||
*/
|
||||
std::string categoriesOPDSFeed(const std::vector<std::string>& categories) const;
|
||||
|
||||
/**
|
||||
* Set the id of the library.
|
||||
*
|
||||
* @param id the id to use.
|
||||
*/
|
||||
void setId(const std::string& id) { this->id = id;}
|
||||
|
||||
/**
|
||||
* Set the title oft the opds stream.
|
||||
*
|
||||
* @param title the title to use.
|
||||
*/
|
||||
void setTitle(const std::string& title) { this->title = title; }
|
||||
void setLibraryId(const std::string& id) { this->libraryId = id;}
|
||||
|
||||
/**
|
||||
* Set the root location used when generating url.
|
||||
@@ -77,13 +88,6 @@ class OPDSDumper
|
||||
*/
|
||||
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
|
||||
|
||||
/**
|
||||
* Set the search url.
|
||||
*
|
||||
* @param searchUrl the search url to use.
|
||||
*/
|
||||
void setSearchDescriptionUrl(const std::string& searchDescriptionUrl) { this->searchDescriptionUrl = searchDescriptionUrl; }
|
||||
|
||||
/**
|
||||
* Set some informations about the search results.
|
||||
*
|
||||
@@ -93,27 +97,13 @@ class OPDSDumper
|
||||
*/
|
||||
void setOpenSearchInfo(int totalResult, int startIndex, int count);
|
||||
|
||||
/**
|
||||
* Set the library to dump.
|
||||
*
|
||||
* @param library The library to dump.
|
||||
*/
|
||||
void setLibrary(Library* library) { this->library = library; }
|
||||
|
||||
protected:
|
||||
kiwix::Library* library;
|
||||
std::string id;
|
||||
std::string title;
|
||||
std::string date;
|
||||
std::string libraryId;
|
||||
std::string rootLocation;
|
||||
std::string searchDescriptionUrl;
|
||||
int m_totalResults;
|
||||
int m_startIndex;
|
||||
int m_count;
|
||||
bool m_isSearchResult = false;
|
||||
|
||||
private:
|
||||
pugi::xml_node handleBook(Book book, pugi::xml_node root_node);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
264
include/reader.h
264
include/reader.h
@@ -21,10 +21,8 @@
|
||||
#define KIWIX_READER_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <zim/article.h>
|
||||
#include <zim/file.h>
|
||||
#include <zim/fileiterator.h>
|
||||
#include <zim/zim.h>
|
||||
#include <zim/archive.h>
|
||||
#include <exception>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
@@ -39,10 +37,47 @@ using namespace std;
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
/**
|
||||
* The SuggestionItem is a helper class that contains the info about a single
|
||||
* suggestion item.
|
||||
*/
|
||||
|
||||
class SuggestionItem
|
||||
{
|
||||
// Functions
|
||||
private:
|
||||
// Create a sugggestion item.
|
||||
explicit SuggestionItem(std::string title, std::string normalizedTitle,
|
||||
std::string path, std::string snippet = "") :
|
||||
title(title),
|
||||
normalizedTitle(normalizedTitle),
|
||||
path(path),
|
||||
snippet(snippet) {}
|
||||
|
||||
public:
|
||||
const std::string getTitle() {return title;}
|
||||
const std::string getNormalizedTitle() {return normalizedTitle;}
|
||||
const std::string getPath() {return path;}
|
||||
const std::string getSnippet() {return snippet;}
|
||||
|
||||
const bool hasSnippet() {return !snippet.empty();}
|
||||
|
||||
// Data
|
||||
private:
|
||||
std::string title;
|
||||
std::string normalizedTitle;
|
||||
std::string path;
|
||||
std::string snippet;
|
||||
|
||||
friend class Reader;
|
||||
};
|
||||
|
||||
/**
|
||||
* The Reader class is the class who allow to get an entry content from a zim
|
||||
* file.
|
||||
*/
|
||||
|
||||
using SuggestionsList_t = std::vector<SuggestionItem>;
|
||||
class Reader
|
||||
{
|
||||
public:
|
||||
@@ -55,8 +90,12 @@ class Reader
|
||||
* unsplitted path as if the file were not splitted
|
||||
* (.zim extesion).
|
||||
*/
|
||||
Reader(const string zimFilePath);
|
||||
~Reader();
|
||||
explicit Reader(const string zimFilePath);
|
||||
#ifndef _WIN32
|
||||
explicit Reader(int fd);
|
||||
Reader(int fd, zim::offset_type offset, zim::size_type size);
|
||||
#endif
|
||||
~Reader() = default;
|
||||
|
||||
/**
|
||||
* Get the number of "displayable" entries in the zim file.
|
||||
@@ -99,17 +138,6 @@ class Reader
|
||||
*/
|
||||
string getId() const;
|
||||
|
||||
/**
|
||||
* Get the url of a random page.
|
||||
*
|
||||
* Deprecated : Use `getRandomPage` instead.
|
||||
*
|
||||
* @return Url of a random page. The page is picked from all entries in
|
||||
* the 'A' namespace.
|
||||
* The main page is excluded from the potential results.
|
||||
*/
|
||||
DEPRECATED string getRandomPageUrl() const;
|
||||
|
||||
/**
|
||||
* Get a random page.
|
||||
*
|
||||
@@ -119,31 +147,6 @@ class Reader
|
||||
*/
|
||||
Entry getRandomPage() const;
|
||||
|
||||
/**
|
||||
* Get the url of the first page.
|
||||
*
|
||||
* Deprecated : Use `getFirstPage` instead.
|
||||
*
|
||||
* @return Url of the first entry in the 'A' namespace.
|
||||
*/
|
||||
DEPRECATED string getFirstPageUrl() const;
|
||||
|
||||
/**
|
||||
* Get the entry of the first page.
|
||||
*
|
||||
* @return The first entry in the 'A' namespace.
|
||||
*/
|
||||
Entry getFirstPage() const;
|
||||
|
||||
/**
|
||||
* Get the url of the main page.
|
||||
*
|
||||
* Deprecated : Use `getMainPage` instead.
|
||||
*
|
||||
* @return Url of the main page as specified in the zim file.
|
||||
*/
|
||||
DEPRECATED string getMainPageUrl() const;
|
||||
|
||||
/**
|
||||
* Get the entry of the main page.
|
||||
*
|
||||
@@ -332,93 +335,15 @@ class Reader
|
||||
*/
|
||||
Entry getEntryFromTitle(const std::string& title) const;
|
||||
|
||||
/**
|
||||
* Get the url of a page specified by a title.
|
||||
*
|
||||
* @param[in] title the title of the page.
|
||||
* @param[out] url the url of the page.
|
||||
* @return True if the page can be found.
|
||||
*/
|
||||
DEPRECATED bool getPageUrlFromTitle(const string& title, string& url) const;
|
||||
|
||||
/**
|
||||
* Get the mimetype of a entry specified by a url.
|
||||
*
|
||||
* @param[in] url the url of the entry.
|
||||
* @param[out] mimeType the mimeType of the entry.
|
||||
* @return True if the mimeType has been found.
|
||||
*/
|
||||
DEPRECATED bool getMimeTypeByUrl(const string& url, string& mimeType) const;
|
||||
|
||||
/**
|
||||
* Get the content of an entry specifed by a url.
|
||||
*
|
||||
* Alias to `getContentByEncodedUrl`
|
||||
*/
|
||||
DEPRECATED bool getContentByUrl(const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType) const;
|
||||
|
||||
/**
|
||||
* Get the content of an entry specified by a url encoded url.
|
||||
*
|
||||
* Equivalent to getContentByDecodedUrl(urlDecode(url), ...).
|
||||
*/
|
||||
DEPRECATED bool getContentByEncodedUrl(const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType,
|
||||
string& baseUrl) const;
|
||||
|
||||
/**
|
||||
* Get the content of an entry specified by an url encoded url.
|
||||
*
|
||||
* Equivalent to getContentByEncodedUrl but without baseUrl.
|
||||
*/
|
||||
DEPRECATED bool getContentByEncodedUrl(const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType) const;
|
||||
|
||||
/**
|
||||
* Get the content of an entry specified by a url.
|
||||
*
|
||||
* @param[in] url The url of the entry.
|
||||
* @param[out] content The content of the entry.
|
||||
* @param[out] title the title of the entry.
|
||||
* @param[out] contentLength The size of the entry (size of content).
|
||||
* @param[out] contentType The mimeType of the entry.
|
||||
* @param[out] baseUrl Return the true url of the entry.
|
||||
* If the specified entry is a redirection, contains
|
||||
* the url of the targeted entry.
|
||||
* @return True if the entry has been found.
|
||||
*/
|
||||
DEPRECATED bool getContentByDecodedUrl(const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType,
|
||||
string& baseUrl) const;
|
||||
/**
|
||||
* Get the content of an entry specified by a url.
|
||||
*
|
||||
* Equivalent to getContentByDecodedUrl but withou the baseUrl.
|
||||
*/
|
||||
DEPRECATED bool getContentByDecodedUrl(const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType) const;
|
||||
|
||||
/**
|
||||
* Search for entries with title starting with prefix (case sensitive).
|
||||
*
|
||||
* Suggestions are stored in an internal vector and can be retrieved using
|
||||
* `getNextSuggestion` method.
|
||||
* This method is not thread safe and is deprecated. Use :
|
||||
* bool searchSuggestions(const string& prefix,
|
||||
* unsigned int suggestionsCount,
|
||||
* SuggestionsList_t& results);
|
||||
*
|
||||
* @param prefix The prefix to search.
|
||||
* @param suggestionsCount How many suggestions to search for.
|
||||
@@ -426,12 +351,49 @@ class Reader
|
||||
* If false, add suggestions to the internal vector
|
||||
* (until internal vector size is suggestionCount (or no more
|
||||
* suggestion))
|
||||
* @return True if some suggestions where added to the internal vector.
|
||||
* @return True if some suggestions have been added to the internal vector.
|
||||
*/
|
||||
bool searchSuggestions(const string& prefix,
|
||||
DEPRECATED bool searchSuggestions(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
const bool reset = true);
|
||||
|
||||
/**
|
||||
* Search for entries with title starting with prefix (case sensitive).
|
||||
*
|
||||
* Suggestions are added to the `result` vector.
|
||||
*
|
||||
* @param prefix The prefix to search.
|
||||
* @param suggestionsCount How many suggestions to search for.
|
||||
* @param result The vector where to store the suggestions.
|
||||
* @return True if some suggestions have been added to the vector.
|
||||
*/
|
||||
|
||||
bool searchSuggestions(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
SuggestionsList_t& resuls);
|
||||
|
||||
/**
|
||||
* Search for entries for the given prefix.
|
||||
*
|
||||
* If the zim file has a internal fulltext index, the suggestions will be
|
||||
* searched using it.
|
||||
* Else the suggestions will be search using `searchSuggestions` while trying
|
||||
* to be smart about case sensitivity (using `getTitleVariants`).
|
||||
*
|
||||
* In any case, suggestions are stored in an internal vector and can be
|
||||
* retrieved using `getNextSuggestion` method.
|
||||
* The internal vector will be reset.
|
||||
* This method is not thread safe and is deprecated. Use :
|
||||
* bool searchSuggestionsSmart(const string& prefix,
|
||||
* unsigned int suggestionsCount,
|
||||
* SuggestionsList_t& results);
|
||||
*
|
||||
* @param prefix The prefix to search for.
|
||||
* @param suggestionsCount How many suggestions to search for.
|
||||
*/
|
||||
DEPRECATED bool searchSuggestionsSmart(const string& prefix,
|
||||
unsigned int suggestionsCount);
|
||||
|
||||
/**
|
||||
* Search for entries for the given prefix.
|
||||
*
|
||||
@@ -446,19 +408,13 @@ class Reader
|
||||
*
|
||||
* @param prefix The prefix to search for.
|
||||
* @param suggestionsCount How many suggestions to search for.
|
||||
* @param results The vector where to store the suggestions
|
||||
* @return True if some suggestions have been added to the results.
|
||||
*/
|
||||
bool searchSuggestionsSmart(const string& prefix,
|
||||
unsigned int suggestionsCount);
|
||||
bool searchSuggestionsSmart(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
SuggestionsList_t& results);
|
||||
|
||||
/**
|
||||
* Check if the url exists in the zim file.
|
||||
*
|
||||
* Deprecated : Use `pathExists` instead.
|
||||
*
|
||||
* @param url the url to check.
|
||||
* @return True if the url exits in the zim file.
|
||||
*/
|
||||
DEPRECATED bool urlExists(const string& url) const;
|
||||
|
||||
/**
|
||||
* Check if the path exists in the zim file.
|
||||
@@ -490,7 +446,7 @@ class Reader
|
||||
* @param[out] title the title of the suggestion.
|
||||
* @return True if title has been set.
|
||||
*/
|
||||
bool getNextSuggestion(string& title);
|
||||
DEPRECATED bool getNextSuggestion(string& title);
|
||||
|
||||
/**
|
||||
* Get the next suggestion title and url.
|
||||
@@ -499,7 +455,7 @@ class Reader
|
||||
* @param[out] url the url of the suggestion.
|
||||
* @return True if title and url have been set.
|
||||
*/
|
||||
bool getNextSuggestion(string& title, string& url);
|
||||
DEPRECATED bool getNextSuggestion(string& title, string& url);
|
||||
|
||||
/**
|
||||
* Get if we can check zim file integrity (has a checksum).
|
||||
@@ -515,16 +471,6 @@ class Reader
|
||||
*/
|
||||
bool isCorrupted() const;
|
||||
|
||||
/**
|
||||
* Parse a full url into a namespace and url.
|
||||
*
|
||||
* @param[in] url The full url ("/N/url").
|
||||
* @param[out] ns The namespace (N).
|
||||
* @param[out] title The url (url).
|
||||
* @return True
|
||||
*/
|
||||
DEPRECATED bool parseUrl(const string& url, char* ns, string& title) const;
|
||||
|
||||
/**
|
||||
* Return the total size of the zim file.
|
||||
*
|
||||
@@ -539,28 +485,14 @@ class Reader
|
||||
*
|
||||
* @return The libzim file handler.
|
||||
*/
|
||||
zim::File* getZimFileHandler() const;
|
||||
|
||||
/**
|
||||
* Get the zim article object associated to a url.
|
||||
*
|
||||
* @param[in] url The url of the article.
|
||||
* @param[out] article The libzim article object.
|
||||
* @return True if the url is good (article.good()).
|
||||
*/
|
||||
DEPRECATED bool getArticleObjectByDecodedUrl(const string& url,
|
||||
zim::Article& article) const;
|
||||
zim::Archive* getZimArchive() const;
|
||||
|
||||
protected:
|
||||
zim::File* zimFileHandler;
|
||||
zim::size_type firstArticleOffset;
|
||||
zim::size_type lastArticleOffset;
|
||||
zim::size_type nsACount;
|
||||
zim::size_type nsICount;
|
||||
std::unique_ptr<zim::Archive> zimArchive;
|
||||
std::string zimFilePath;
|
||||
|
||||
std::vector<std::vector<std::string>> suggestions;
|
||||
std::vector<std::vector<std::string>>::iterator suggestionsOffset;
|
||||
SuggestionsList_t suggestions;
|
||||
SuggestionsList_t::iterator suggestionsOffset;
|
||||
|
||||
private:
|
||||
std::map<const std::string, unsigned int> parseCounterMetadata() const;
|
||||
|
||||
@@ -60,6 +60,13 @@ class SearchRenderer
|
||||
*/
|
||||
void setSearchProtocolPrefix(const std::string& prefix);
|
||||
|
||||
/**
|
||||
* set result count per page
|
||||
*/
|
||||
void setPageLength(unsigned int pageLength){
|
||||
this->pageLength = pageLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the html page with the resutls of the search.
|
||||
*/
|
||||
@@ -73,10 +80,9 @@ class SearchRenderer
|
||||
std::string searchPattern;
|
||||
std::string protocolPrefix;
|
||||
std::string searchProtocolPrefix;
|
||||
unsigned int resultCountPerPage;
|
||||
unsigned int pageLength;
|
||||
unsigned int estimatedResultCount;
|
||||
unsigned int resultStart;
|
||||
unsigned int resultEnd;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -27,11 +27,10 @@
|
||||
#include <cctype>
|
||||
#include <locale>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "tools/pathTools.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include "kiwix_config.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -49,7 +48,7 @@ class Result
|
||||
virtual std::string get_content() = 0;
|
||||
virtual int get_wordCount() = 0;
|
||||
virtual int get_size() = 0;
|
||||
virtual int get_readerIndex() = 0;
|
||||
virtual std::string get_zimId() = 0;
|
||||
};
|
||||
|
||||
struct SearcherInternal;
|
||||
@@ -89,7 +88,7 @@ class Searcher
|
||||
* @param resultEnd the end offset of the search results (used for pagination).
|
||||
* @param verbose print some info on stdout if true.
|
||||
*/
|
||||
void search(std::string& search,
|
||||
void search(const std::string& search,
|
||||
unsigned int resultStart,
|
||||
unsigned int resultEnd,
|
||||
const bool verbose = false);
|
||||
@@ -155,7 +154,7 @@ class Searcher
|
||||
const bool verbose = false);
|
||||
|
||||
std::vector<Reader*> readers;
|
||||
SearcherInternal* internal;
|
||||
std::unique_ptr<SearcherInternal> internal;
|
||||
std::string searchPattern;
|
||||
unsigned int estimatedResultCount;
|
||||
unsigned int resultStart;
|
||||
|
||||
@@ -56,7 +56,9 @@ namespace kiwix
|
||||
void setNbThreads(int threads) { m_nbThreads = threads; }
|
||||
void setVerbose(bool verbose) { m_verbose = verbose; }
|
||||
void setTaskbar(bool withTaskbar, bool withLibraryButton)
|
||||
{ m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; }
|
||||
{ m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; }
|
||||
void setBlockExternalLinks(bool blockExternalLinks)
|
||||
{ m_blockExternalLinks = blockExternalLinks; }
|
||||
|
||||
protected:
|
||||
Library* mp_library;
|
||||
@@ -68,6 +70,7 @@ namespace kiwix
|
||||
bool m_verbose = false;
|
||||
bool m_withTaskbar = true;
|
||||
bool m_withLibraryButton = true;
|
||||
bool m_blockExternalLinks = false;
|
||||
std::unique_ptr<InternalServer> mp_server;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
|
||||
#ifndef KIWIXLIB_TOOL_LOCK_H
|
||||
#define KIWIXLIB_TOOL_LOCK_H
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
class Lock
|
||||
{
|
||||
public:
|
||||
explicit Lock(pthread_mutex_t* mutex) :
|
||||
mp_mutex(mutex)
|
||||
{
|
||||
pthread_mutex_lock(mp_mutex);
|
||||
}
|
||||
~Lock() {
|
||||
if (mp_mutex != nullptr) {
|
||||
pthread_mutex_unlock(mp_mutex);
|
||||
}
|
||||
}
|
||||
Lock(Lock && other) :
|
||||
mp_mutex(other.mp_mutex)
|
||||
{
|
||||
other.mp_mutex = nullptr;
|
||||
}
|
||||
Lock & operator=(Lock && other)
|
||||
{
|
||||
mp_mutex = other.mp_mutex;
|
||||
other.mp_mutex = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
pthread_mutex_t* mp_mutex;
|
||||
|
||||
Lock(Lock const &) = delete;
|
||||
Lock & operator=(Lock const &) = delete;
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endif //KIWIXLIB_TOOL_LOCK_H
|
||||
@@ -21,6 +21,10 @@
|
||||
#define KIWIX_OTHERTOOLS_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <zim/zim.h>
|
||||
#include <mustache.hpp>
|
||||
|
||||
namespace pugi {
|
||||
class xml_node;
|
||||
@@ -28,9 +32,25 @@ namespace pugi {
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
void sleep(unsigned int milliseconds);
|
||||
std::string nodeToString(const pugi::xml_node& node);
|
||||
std::string converta2toa3(const std::string& a2code);
|
||||
void sleep(unsigned int milliseconds);
|
||||
std::string nodeToString(const pugi::xml_node& node);
|
||||
std::string converta2toa3(const std::string& a2code);
|
||||
|
||||
/*
|
||||
* Convert all format tag string to new format
|
||||
*/
|
||||
std::vector<std::string> convertTags(const std::string& tags_str);
|
||||
std::string getTagValueFromTagList(const std::vector<std::string>& tagList,
|
||||
const std::string& tagName);
|
||||
bool convertStrToBool(const std::string& value);
|
||||
|
||||
using MimeCounterType = std::map<const std::string, zim::entry_index_type>;
|
||||
MimeCounterType parseMimetypeCounter(const std::string& counterData);
|
||||
|
||||
std::string gen_date_str();
|
||||
std::string gen_uuid(const std::string& s);
|
||||
|
||||
std::string render_template(const std::string& template_str, kainjow::mustache::data data);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string WideToUtf8(const std::wstring& wstr);
|
||||
std::wstring Utf8ToWide(const std::string& str);
|
||||
#endif
|
||||
bool isRelativePath(const std::string& path);
|
||||
std::string computeAbsolutePath(const std::string& path, const std::string& relativePath);
|
||||
std::string computeRelativePath(const std::string& path, const std::string& absolutePath);
|
||||
|
||||
@@ -29,5 +29,8 @@ std::string replaceRegex(const std::string& content,
|
||||
std::string appendToFirstOccurence(const std::string& content,
|
||||
const std::string& regex,
|
||||
const std::string& replacement);
|
||||
std::string prependToFirstOccurence(const std::string& content,
|
||||
const std::string& regex,
|
||||
const std::string& replacement);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -43,7 +43,7 @@ void loadICUExternalTables();
|
||||
std::string urlEncode(const std::string& value, bool encodeReserved = false);
|
||||
std::string urlDecode(const std::string& value, bool component = false);
|
||||
|
||||
std::vector<std::string> split(const std::string&, const std::string&, bool trimEmpty = true);
|
||||
std::vector<std::string> split(const std::string& str, const std::string& delims, bool trimEmpty = true, bool keepDelim = false);
|
||||
std::string join(const std::vector<std::string>& list, const std::string& sep);
|
||||
|
||||
std::string ucAll(const std::string& word);
|
||||
|
||||
53
meson.build
53
meson.build
@@ -1,37 +1,68 @@
|
||||
project('kiwix-lib', 'cpp',
|
||||
version : '8.2.2', # Also change this in android-kiwix-lib-publisher/kiwixLibAndroid/build.gradle
|
||||
license : 'GPL',
|
||||
project('libkiwix', 'cpp',
|
||||
version : '10.0.0', # Also change this in android-kiwix-lib-publisher/kiwixLibAndroid/build.gradle
|
||||
license : 'GPLv3+',
|
||||
default_options : ['c_std=c11', 'cpp_std=c++11', 'werror=true'])
|
||||
|
||||
compiler = meson.get_compiler('cpp')
|
||||
|
||||
static_deps = get_option('android') or get_option('default_library') == 'static'
|
||||
if get_option('android')
|
||||
wrapper = get_option('wrapper')
|
||||
|
||||
static_deps = wrapper.contains('android') or wrapper.contains('java') or get_option('default_library') == 'static'
|
||||
if wrapper.contains('android')
|
||||
extra_libs = ['-llog']
|
||||
else
|
||||
extra_libs = []
|
||||
endif
|
||||
|
||||
thread_dep = dependency('threads')
|
||||
if wrapper.contains('java')
|
||||
add_languages('java')
|
||||
endif
|
||||
|
||||
# See https://github.com/kiwix/libkiwix/issues/371
|
||||
if ['arm', 'mips', 'm68k', 'ppc', 'sh4'].contains(target_machine.cpu_family())
|
||||
extra_libs += '-latomic'
|
||||
endif
|
||||
|
||||
if (compiler.get_id() == 'gcc' and build_machine.system() == 'linux') or target_machine.system() == 'freebsd'
|
||||
# C++ std::thread is implemented using pthread on linux by gcc
|
||||
thread_dep = dependency('threads')
|
||||
else
|
||||
thread_dep = dependency('', required:false)
|
||||
endif
|
||||
libicu_dep = dependency('icu-i18n', static:static_deps)
|
||||
libzim_dep = dependency('libzim', version : '>=5.0.0', static:static_deps)
|
||||
pugixml_dep = dependency('pugixml', static:static_deps)
|
||||
libcurl_dep = dependency('libcurl', static:static_deps)
|
||||
microhttpd_dep = dependency('libmicrohttpd', static:static_deps)
|
||||
zlib_dep = dependency('zlib', static:static_deps)
|
||||
xapian_dep = dependency('xapian-core', static:static_deps)
|
||||
|
||||
if not compiler.has_header('mustache.hpp')
|
||||
if compiler.has_header('mustache.hpp')
|
||||
extra_include = []
|
||||
elif compiler.has_header('mustache.hpp', args: '-I/usr/include/kainjow')
|
||||
extra_include = ['/usr/include/kainjow']
|
||||
else
|
||||
error('Cannot found header mustache.hpp')
|
||||
endif
|
||||
|
||||
libzim_dep = dependency('libzim', version : '>=7.0.0', static:static_deps)
|
||||
if not compiler.has_header_symbol('zim/zim.h', 'LIBZIM_WITH_XAPIAN')
|
||||
error('Libzim seems to be compiled without xapian. Xapian support is mandatory.')
|
||||
endif
|
||||
|
||||
|
||||
extra_cflags = ''
|
||||
if target_machine.system() == 'windows' and static_deps
|
||||
add_project_arguments('-DCURL_STATICLIB', language : 'cpp')
|
||||
extra_cflags += '-DCURL_STATICLIB'
|
||||
endif
|
||||
|
||||
all_deps = [thread_dep, libicu_dep, libzim_dep, pugixml_dep, libcurl_dep, microhttpd_dep]
|
||||
if target_machine.system() == 'windows'
|
||||
add_project_arguments('-DNOMINMAX', language: 'cpp')
|
||||
endif
|
||||
|
||||
inc = include_directories('include')
|
||||
all_deps = [thread_dep, libicu_dep, libzim_dep, pugixml_dep, libcurl_dep, microhttpd_dep, zlib_dep, xapian_dep]
|
||||
|
||||
inc = include_directories('include', extra_include)
|
||||
|
||||
conf = configuration_data()
|
||||
conf.set('VERSION', '"@0@"'.format(meson.project_version()))
|
||||
@@ -48,7 +79,7 @@ subdir('static')
|
||||
subdir('src')
|
||||
subdir('test')
|
||||
|
||||
pkg_requires = ['libzim', 'icu-i18n', 'pugixml', 'libcurl']
|
||||
pkg_requires = ['libzim', 'icu-i18n', 'pugixml', 'libcurl', 'libmicrohttpd', 'xapian-core']
|
||||
|
||||
pkg_conf = configuration_data()
|
||||
pkg_conf.set('prefix', get_option('prefix'))
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
option('android', type : 'boolean', value : false,
|
||||
description : 'Do we make a kiwix-lib for android')
|
||||
option('wrapper', type:'array', choices:['java', 'android'], value:[],
|
||||
description: 'The wrapper to generate.')
|
||||
|
||||
20
scripts/kiwix-compile-resources.1
Normal file
20
scripts/kiwix-compile-resources.1
Normal file
@@ -0,0 +1,20 @@
|
||||
.TH KIWIX-COMPILE-RESOURCES "1" "August 2017" "Kiwix" "User Commands"
|
||||
.SH NAME
|
||||
kiwix-compile-resources \- helper to compile and generate some Kiwix resources
|
||||
.SH SYNOPSIS
|
||||
\fBkiwix\-compile\-resources\fR [\-h] [\-\-cxxfile CXXFILE] [\-\-hfile HFILE] resource_file\fR
|
||||
.SH DESCRIPTION
|
||||
.TP
|
||||
resource_file
|
||||
The list of resources to compile.
|
||||
.TP
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
show a help message and exit
|
||||
.TP
|
||||
\fB\-\-cxxfile\fR CXXFILE
|
||||
The Cpp file name to generate
|
||||
.TP
|
||||
\fB\-\-hfile\fR HFILE
|
||||
The h file name to generate
|
||||
.SH AUTHOR
|
||||
Matthieu Gautier <mgautier@kymeria.fr>
|
||||
@@ -2,3 +2,5 @@
|
||||
res_compiler = find_program('kiwix-compile-resources')
|
||||
|
||||
install_data(res_compiler.path(), install_dir:get_option('bindir'))
|
||||
|
||||
install_man('kiwix-compile-resources.1')
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Emmanuel Engelhart <kelson@kiwix.org>
|
||||
* Copyright (C) 2017 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include "org_kiwix_kiwixlib_JNIKiwixLibrary.h"
|
||||
|
||||
#include "library.h"
|
||||
#include "reader.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* Kiwix Reader JNI functions */
|
||||
JNIEXPORT jlong JNICALL Java_org_kiwix_kiwixlib_JNIKiwixLibrary_getNativeLibrary(
|
||||
JNIEnv* env, jobject obj)
|
||||
{
|
||||
__android_log_print(ANDROID_LOG_INFO, "kiwix", "Attempting to create library");
|
||||
Lock l;
|
||||
try {
|
||||
kiwix::Library* library = new kiwix::Library();
|
||||
return reinterpret_cast<jlong>(new Handle<kiwix::Library>(library));
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", "Error creating ZIM library");
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", e.what());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_kiwix_kiwixlib_JNIKiwixLibrary_dispose(JNIEnv* env, jobject obj)
|
||||
{
|
||||
Handle<kiwix::Library>::dispose(env, obj);
|
||||
}
|
||||
|
||||
#define LIBRARY (Handle<kiwix::Library>::getHandle(env, obj))
|
||||
|
||||
/* Kiwix library functions */
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_kiwix_kiwixlib_JNIKiwixLibrary_addBook(JNIEnv* env, jobject obj, jstring path)
|
||||
{
|
||||
std::string cPath = jni2c(path, env);
|
||||
bool ret;
|
||||
|
||||
try {
|
||||
kiwix::Reader reader(cPath);
|
||||
kiwix::Book book;
|
||||
book.update(reader);
|
||||
ret = LIBRARY->addBook(book);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to add the book");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
ret = false;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
kiwix_jni = custom_target('jni',
|
||||
input: ['org/kiwix/kiwixlib/JNIICU.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixReader.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixLibrary.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixSearcher.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixServer.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixInt.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixString.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixBool.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixException.java',
|
||||
'org/kiwix/kiwixlib/Pair.java'],
|
||||
output: ['org_kiwix_kiwixlib_JNIKiwix.h',
|
||||
'org_kiwix_kiwixlib_JNIKiwixReader.h',
|
||||
'org_kiwix_kiwixlib_JNIKiwixLibrary.h',
|
||||
'org_kiwix_kiwixlib_JNIKiwixServer.h',
|
||||
'org_kiwix_kiwixlib_JNIKiwixSearcher.h',
|
||||
'org_kiwix_kiwixlib_JNIKiwixSearcher_Result.h'],
|
||||
command:['javac', '-d', '@OUTDIR@', '-h', '@OUTDIR@', '@INPUT@']
|
||||
)
|
||||
|
||||
kiwix_sources += [
|
||||
'android/kiwixicu.cpp',
|
||||
'android/kiwixreader.cpp',
|
||||
'android/kiwixlibrary.cpp',
|
||||
'android/kiwixsearcher.cpp',
|
||||
'android/kiwixserver.cpp',
|
||||
kiwix_jni]
|
||||
|
||||
install_subdir('org', install_dir: 'kiwix-lib/java')
|
||||
install_subdir('res', install_dir: 'kiwix-lib')
|
||||
install_data('AndroidManifest.xml', install_dir: 'kiwix-lib')
|
||||
@@ -1,157 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Emmanuel Engelhart <kelson@kiwix.org>
|
||||
* Copyright (C) 2017 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#ifndef _ANDROID_JNI_UTILS_H
|
||||
#define _ANDROID_JNI_UTILS_H
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <string>
|
||||
|
||||
extern pthread_mutex_t globalLock;
|
||||
|
||||
inline jfieldID getHandleField(JNIEnv* env, jobject obj)
|
||||
{
|
||||
jclass c = env->GetObjectClass(obj);
|
||||
// J is the type signature for long:
|
||||
return env->GetFieldID(c, "nativeHandle", "J");
|
||||
}
|
||||
|
||||
class Lock
|
||||
{
|
||||
protected:
|
||||
pthread_mutex_t* lock;
|
||||
|
||||
public:
|
||||
Lock() : lock(&globalLock) { pthread_mutex_lock(lock); }
|
||||
Lock(const Lock&) = delete;
|
||||
Lock& operator=(const Lock&) = delete;
|
||||
Lock(Lock&& other) : lock(&globalLock) { other.lock = nullptr; }
|
||||
virtual ~Lock()
|
||||
{
|
||||
if (lock) {
|
||||
pthread_mutex_unlock(lock);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
class LockedHandle;
|
||||
|
||||
template <class T>
|
||||
class Handle
|
||||
{
|
||||
protected:
|
||||
T* h;
|
||||
|
||||
public:
|
||||
Handle(T* h) : h(h){};
|
||||
|
||||
// No destructor. This must and will be handled by dispose method.
|
||||
|
||||
static LockedHandle<T> getHandle(JNIEnv* env, jobject obj)
|
||||
{
|
||||
jlong handle = env->GetLongField(obj, getHandleField(env, obj));
|
||||
return LockedHandle<T>(reinterpret_cast<Handle<T>*>(handle));
|
||||
}
|
||||
|
||||
static void dispose(JNIEnv* env, jobject obj)
|
||||
{
|
||||
auto lHandle = getHandle(env, obj);
|
||||
auto handle = lHandle.h;
|
||||
delete handle->h;
|
||||
delete handle;
|
||||
}
|
||||
friend class LockedHandle<T>;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct LockedHandle : public Lock {
|
||||
Handle<T>* h;
|
||||
LockedHandle(Handle<T>* h) : h(h) {}
|
||||
T* operator->() { return h->h; }
|
||||
T* operator*() { return h->h; }
|
||||
operator bool() const { return (h->h != nullptr); }
|
||||
};
|
||||
|
||||
/* c2jni type conversion functions */
|
||||
inline jboolean c2jni(const bool& val) { return val ? JNI_TRUE : JNI_FALSE; }
|
||||
inline jstring c2jni(const std::string& val, JNIEnv* env)
|
||||
{
|
||||
return env->NewStringUTF(val.c_str());
|
||||
}
|
||||
|
||||
inline jint c2jni(const int val) { return (jint)val; }
|
||||
inline jint c2jni(const unsigned val) { return (unsigned)val; }
|
||||
/* jni2c type conversion functions */
|
||||
inline bool jni2c(const jboolean& val) { return val == JNI_TRUE; }
|
||||
inline std::string jni2c(const jstring& val, JNIEnv* env)
|
||||
{
|
||||
const char* chars = env->GetStringUTFChars(val, 0);
|
||||
std::string ret(chars);
|
||||
env->ReleaseStringUTFChars(val, chars);
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline int jni2c(const jint val) { return (int)val; }
|
||||
/* Method to deal with variable passed by reference */
|
||||
inline std::string getStringObjValue(const jobject obj, JNIEnv* env)
|
||||
{
|
||||
jclass objClass = env->GetObjectClass(obj);
|
||||
jfieldID objFid = env->GetFieldID(objClass, "value", "Ljava/lang/String;");
|
||||
jstring jstr = (jstring)env->GetObjectField(obj, objFid);
|
||||
return jni2c(jstr, env);
|
||||
}
|
||||
inline void setStringObjValue(const std::string& value,
|
||||
const jobject obj,
|
||||
JNIEnv* env)
|
||||
{
|
||||
jclass objClass = env->GetObjectClass(obj);
|
||||
jfieldID objFid = env->GetFieldID(objClass, "value", "Ljava/lang/String;");
|
||||
env->SetObjectField(obj, objFid, c2jni(value, env));
|
||||
}
|
||||
|
||||
inline void setIntObjValue(const int value, const jobject obj, JNIEnv* env)
|
||||
{
|
||||
jclass objClass = env->GetObjectClass(obj);
|
||||
jfieldID objFid = env->GetFieldID(objClass, "value", "I");
|
||||
env->SetIntField(obj, objFid, value);
|
||||
}
|
||||
|
||||
inline void setBoolObjValue(const bool value, const jobject obj, JNIEnv* env)
|
||||
{
|
||||
jclass objClass = env->GetObjectClass(obj);
|
||||
jfieldID objFid = env->GetFieldID(objClass, "value", "Z");
|
||||
env->SetIntField(obj, objFid, c2jni(value));
|
||||
}
|
||||
|
||||
inline void setPairObjValue(const std::string& filename, const int offset,
|
||||
const jobject obj, JNIEnv* env)
|
||||
{
|
||||
jclass objClass = env->GetObjectClass(obj);
|
||||
jfieldID filenameFid = env->GetFieldID(objClass, "filename", "Ljava/lang/String;");
|
||||
env->SetObjectField(obj, filenameFid, c2jni(filename, env));
|
||||
jfieldID offsetFid = env->GetFieldID(objClass, "offset", "I");
|
||||
env->SetIntField(obj, offsetFid, offset);
|
||||
}
|
||||
|
||||
#endif // _ANDROID_JNI_UTILS_H
|
||||
@@ -19,6 +19,11 @@
|
||||
#endif
|
||||
|
||||
|
||||
#define LOG_ARIA_ERROR() \
|
||||
{ \
|
||||
std::cerr << "ERROR: aria2 RPC request failed. (" << res << ")." << std::endl; \
|
||||
std::cerr << (m_curlErrorBuffer[0] ? m_curlErrorBuffer.get() : curl_easy_strerror(res)) << std::endl; \
|
||||
}
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
@@ -26,8 +31,8 @@ Aria2::Aria2():
|
||||
mp_aria(nullptr),
|
||||
m_port(42042),
|
||||
m_secret("kiwixariarpc"),
|
||||
mp_curl(nullptr),
|
||||
m_lock(PTHREAD_MUTEX_INITIALIZER)
|
||||
m_curlErrorBuffer(new char[CURL_ERROR_SIZE]),
|
||||
mp_curl(nullptr)
|
||||
{
|
||||
m_downloadDir = getDataDirectory();
|
||||
makeDirectory(m_downloadDir);
|
||||
@@ -58,6 +63,7 @@ Aria2::Aria2():
|
||||
// Try to use a potential installed aria2c.
|
||||
callCmd.push_back(ARIA2_CMD);
|
||||
}
|
||||
callCmd.push_back("--follow-metalink=mem");
|
||||
callCmd.push_back("--enable-rpc");
|
||||
callCmd.push_back(rpc_secret.c_str());
|
||||
callCmd.push_back(rpc_port.c_str());
|
||||
@@ -78,42 +84,38 @@ Aria2::Aria2():
|
||||
callCmd.push_back("--max-concurrent-downloads=42");
|
||||
callCmd.push_back("--rpc-max-request-size=6M");
|
||||
callCmd.push_back("--file-allocation=none");
|
||||
std::string launchCmd;
|
||||
for (auto &cmd : callCmd) {
|
||||
m_launchCmd.append(cmd).append(" ");
|
||||
launchCmd.append(cmd).append(" ");
|
||||
}
|
||||
mp_aria = Subprocess::run(callCmd);
|
||||
mp_curl = curl_easy_init();
|
||||
char errbuf[CURL_ERROR_SIZE];
|
||||
|
||||
curl_easy_setopt(mp_curl, CURLOPT_URL, "http://localhost/rpc");
|
||||
curl_easy_setopt(mp_curl, CURLOPT_PORT, m_port);
|
||||
curl_easy_setopt(mp_curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(mp_curl, CURLOPT_ERRORBUFFER, errbuf);
|
||||
curl_easy_setopt(mp_curl, CURLOPT_ERRORBUFFER, m_curlErrorBuffer.get());
|
||||
|
||||
int watchdog = 50;
|
||||
while(--watchdog) {
|
||||
sleep(10);
|
||||
errbuf[0] = 0;
|
||||
m_curlErrorBuffer[0] = 0;
|
||||
auto res = curl_easy_perform(mp_curl);
|
||||
if (res == CURLE_OK) {
|
||||
break;
|
||||
} else if (watchdog == 1) {
|
||||
std::cerr <<" curl_easy_perform() failed." << std::endl;
|
||||
fprintf(stderr, "\nlibcurl: (%d) ", res);
|
||||
if (errbuf[0] != 0) {
|
||||
std::cerr << errbuf << std::endl;
|
||||
} else {
|
||||
std::cerr << curl_easy_strerror(res) << std::endl;
|
||||
}
|
||||
LOG_ARIA_ERROR();
|
||||
}
|
||||
}
|
||||
if (!watchdog) {
|
||||
curl_easy_cleanup(mp_curl);
|
||||
throw std::runtime_error("Cannot connect to aria2c rpc");
|
||||
throw std::runtime_error("Cannot connect to aria2c rpc. Aria2c launch cmd : " + launchCmd);
|
||||
}
|
||||
}
|
||||
|
||||
Aria2::~Aria2()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
curl_easy_cleanup(mp_curl);
|
||||
}
|
||||
|
||||
@@ -125,47 +127,56 @@ void Aria2::close()
|
||||
|
||||
size_t write_callback_to_iss(char* ptr, size_t size, size_t nmemb, void* userdata)
|
||||
{
|
||||
auto str = static_cast<std::stringstream*>(userdata);
|
||||
str->write(ptr, nmemb);
|
||||
auto outStream = static_cast<std::stringstream*>(userdata);
|
||||
outStream->write(ptr, nmemb);
|
||||
return nmemb;
|
||||
}
|
||||
|
||||
std::string Aria2::doRequest(const MethodCall& methodCall)
|
||||
{
|
||||
pthread_mutex_lock(&m_lock);
|
||||
auto requestContent = methodCall.toString();
|
||||
std::stringstream stringstream;
|
||||
std::stringstream outStream;
|
||||
CURLcode res;
|
||||
curl_easy_setopt(mp_curl, CURLOPT_POSTFIELDSIZE, requestContent.size());
|
||||
curl_easy_setopt(mp_curl, CURLOPT_POSTFIELDS, requestContent.c_str());
|
||||
curl_easy_setopt(mp_curl, CURLOPT_WRITEFUNCTION, &write_callback_to_iss);
|
||||
curl_easy_setopt(mp_curl, CURLOPT_WRITEDATA, &stringstream);
|
||||
res = curl_easy_perform(mp_curl);
|
||||
if (res == CURLE_OK) {
|
||||
long response_code;
|
||||
long response_code;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
curl_easy_setopt(mp_curl, CURLOPT_POSTFIELDSIZE, requestContent.size());
|
||||
curl_easy_setopt(mp_curl, CURLOPT_POSTFIELDS, requestContent.c_str());
|
||||
curl_easy_setopt(mp_curl, CURLOPT_WRITEFUNCTION, &write_callback_to_iss);
|
||||
curl_easy_setopt(mp_curl, CURLOPT_WRITEDATA, &outStream);
|
||||
m_curlErrorBuffer[0] = 0;
|
||||
res = curl_easy_perform(mp_curl);
|
||||
if (res != CURLE_OK) {
|
||||
LOG_ARIA_ERROR();
|
||||
throw std::runtime_error("Cannot perform request");
|
||||
}
|
||||
curl_easy_getinfo(mp_curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
pthread_mutex_unlock(&m_lock);
|
||||
if (response_code != 200) {
|
||||
throw std::runtime_error("Invalid return code from aria");
|
||||
}
|
||||
auto responseContent = stringstream.str();
|
||||
MethodResponse response(responseContent);
|
||||
if (response.isFault()) {
|
||||
throw AriaError(response.getFault().getFaultString());
|
||||
}
|
||||
return responseContent;
|
||||
}
|
||||
pthread_mutex_unlock(&m_lock);
|
||||
throw std::runtime_error("Cannot perform request");
|
||||
|
||||
auto responseContent = outStream.str();
|
||||
if (response_code != 200) {
|
||||
std::cerr << "ERROR: Invalid return code (" << response_code << ") from aria :" << std::endl;
|
||||
std::cerr << responseContent << std::endl;
|
||||
throw std::runtime_error("Invalid return code from aria");
|
||||
}
|
||||
|
||||
MethodResponse response(responseContent);
|
||||
if (response.isFault()) {
|
||||
throw AriaError(response.getFault().getFaultString());
|
||||
}
|
||||
return responseContent;
|
||||
}
|
||||
|
||||
std::string Aria2::addUri(const std::vector<std::string>& uris)
|
||||
std::string Aria2::addUri(const std::vector<std::string>& uris, const std::vector<std::pair<std::string, std::string>>& options)
|
||||
{
|
||||
MethodCall methodCall("aria2.addUri", m_secret);
|
||||
auto uriParams = methodCall.newParamValue().getArray();
|
||||
for (auto& uri : uris) {
|
||||
uriParams.addValue().set(uri);
|
||||
}
|
||||
for (auto& option : options) {
|
||||
methodCall.newParamValue().getStruct().addMember(option.first).getValue().set(option.second);
|
||||
}
|
||||
auto ret = doRequest(methodCall);
|
||||
MethodResponse response(ret);
|
||||
return response.getParamValue(0).getAsS();
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
#include "xmlrpc.h"
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <curl/curl.h>
|
||||
#include <pthread.h>
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
@@ -24,9 +24,9 @@ class Aria2
|
||||
int m_port;
|
||||
std::string m_secret;
|
||||
std::string m_downloadDir;
|
||||
std::unique_ptr<char[]> m_curlErrorBuffer;
|
||||
CURL* mp_curl;
|
||||
pthread_mutex_t m_lock;
|
||||
std::string m_launchCmd;
|
||||
std::mutex m_lock;
|
||||
|
||||
std::string doRequest(const MethodCall& methodCall);
|
||||
|
||||
@@ -35,7 +35,7 @@ class Aria2
|
||||
virtual ~Aria2();
|
||||
void close();
|
||||
|
||||
std::string addUri(const std::vector<std::string>& uri);
|
||||
std::string addUri(const std::vector<std::string>& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
|
||||
std::string tellStatus(const std::string& gid, const std::vector<std::string>& statusKey);
|
||||
std::vector<std::string> tellActive();
|
||||
std::vector<std::string> tellWaiting();
|
||||
@@ -44,7 +44,6 @@ class Aria2
|
||||
void pause(const std::string& gid);
|
||||
void unpause(const std::string& gid);
|
||||
void remove(const std::string& gid);
|
||||
const std::string &getLaunchCmd() { return m_launchCmd; };
|
||||
};
|
||||
|
||||
}; //end namespace kiwix
|
||||
|
||||
100
src/book.cpp
100
src/book.cpp
@@ -23,6 +23,7 @@
|
||||
#include "tools/base64.h"
|
||||
#include "tools/regexTools.h"
|
||||
#include "tools/networkTools.h"
|
||||
#include "tools/otherTools.h"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
@@ -44,44 +45,51 @@ bool Book::update(const kiwix::Book& other)
|
||||
if (m_readOnly)
|
||||
return false;
|
||||
|
||||
if (m_id != other.m_id)
|
||||
return false;
|
||||
|
||||
m_readOnly = other.m_readOnly;
|
||||
m_path = other.m_path;
|
||||
m_pathValid = other.m_pathValid;
|
||||
m_title = other.m_title;
|
||||
m_description = other.m_description;
|
||||
m_language = other.m_language;
|
||||
m_creator = other.m_creator;
|
||||
m_publisher = other.m_publisher;
|
||||
m_date = other.m_date;
|
||||
m_url = other.m_url;
|
||||
m_name = other.m_name;
|
||||
m_flavour = other.m_flavour;
|
||||
m_tags = other.m_tags;
|
||||
m_category = other.m_category;
|
||||
m_origId = other.m_origId;
|
||||
m_articleCount = other.m_articleCount;
|
||||
m_mediaCount = other.m_mediaCount;
|
||||
m_size = other.m_size;
|
||||
m_favicon = other.m_favicon;
|
||||
m_faviconMimeType = other.m_faviconMimeType;
|
||||
m_faviconUrl = other.m_faviconUrl;
|
||||
|
||||
if (m_path.empty()) {
|
||||
m_path = other.m_path;
|
||||
m_pathValid = other.m_pathValid;
|
||||
}
|
||||
m_downloadId = other.m_downloadId;
|
||||
|
||||
if (m_url.empty()) {
|
||||
m_url = other.m_url;
|
||||
}
|
||||
|
||||
if (m_tags.empty()) {
|
||||
m_tags = other.m_tags;
|
||||
}
|
||||
|
||||
if (m_name.empty()) {
|
||||
m_name = other.m_name;
|
||||
}
|
||||
|
||||
if (m_faviconMimeType.empty()) {
|
||||
m_favicon = other.m_favicon;
|
||||
m_faviconMimeType = other.m_faviconMimeType;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Book::update(const kiwix::Reader& reader)
|
||||
{
|
||||
m_path = reader.getZimFilePath();
|
||||
m_pathValid = true;
|
||||
m_id = reader.getId();
|
||||
m_title = reader.getTitle();
|
||||
m_description = reader.getDescription();
|
||||
m_language = reader.getLanguage();
|
||||
m_date = reader.getDate();
|
||||
m_creator = reader.getCreator();
|
||||
m_publisher = reader.getPublisher();
|
||||
m_title = reader.getTitle();
|
||||
m_date = reader.getDate();
|
||||
m_name = reader.getName();
|
||||
m_flavour = reader.getFlavour();
|
||||
m_tags = reader.getTags();
|
||||
m_category = getCategoryFromTags();
|
||||
m_origId = reader.getOrigId();
|
||||
m_articleCount = reader.getArticleCount();
|
||||
m_mediaCount = reader.getMediaCount();
|
||||
@@ -100,24 +108,29 @@ void Book::updateFromXml(const pugi::xml_node& node, const std::string& baseDir)
|
||||
path = computeAbsolutePath(baseDir, path);
|
||||
}
|
||||
m_path = path;
|
||||
m_pathValid = fileExists(path);
|
||||
m_title = ATTR("title");
|
||||
m_name = ATTR("name");
|
||||
m_tags = ATTR("tags");
|
||||
m_description = ATTR("description");
|
||||
m_language = ATTR("language");
|
||||
m_date = ATTR("date");
|
||||
m_creator = ATTR("creator");
|
||||
m_publisher = ATTR("publisher");
|
||||
m_date = ATTR("date");
|
||||
m_url = ATTR("url");
|
||||
m_name = ATTR("name");
|
||||
m_flavour = ATTR("flavour");
|
||||
m_tags = ATTR("tags");
|
||||
m_origId = ATTR("origId");
|
||||
m_articleCount = strtoull(ATTR("articleCount"), 0, 0);
|
||||
m_mediaCount = strtoull(ATTR("mediaCount"), 0, 0);
|
||||
m_size = strtoull(ATTR("size"), 0, 0) << 10;
|
||||
m_favicon = base64_decode(ATTR("favicon"));
|
||||
m_faviconMimeType = ATTR("faviconMimeType");
|
||||
m_faviconUrl = ATTR("faviconUrl");
|
||||
try {
|
||||
m_downloadId = ATTR("downloadId");
|
||||
} catch(...) {}
|
||||
const auto catattr = node.attribute("category");
|
||||
m_category = catattr.empty() ? getCategoryFromTags() : catattr.value();
|
||||
}
|
||||
#undef ATTR
|
||||
|
||||
@@ -137,12 +150,20 @@ void Book::updateFromOpds(const pugi::xml_node& node, const std::string& urlHost
|
||||
if (!m_id.compare(0, 9, "urn:uuid:")) {
|
||||
m_id.erase(0, 9);
|
||||
}
|
||||
// No path on opds.
|
||||
m_title = VALUE("title");
|
||||
m_description = VALUE("description");
|
||||
m_description = VALUE("summary");
|
||||
m_language = VALUE("language");
|
||||
m_date = fromOpdsDate(VALUE("updated"));
|
||||
m_creator = node.child("author").child("name").child_value();
|
||||
m_publisher = node.child("publisher").child("name").child_value();
|
||||
m_date = fromOpdsDate(VALUE("updated"));
|
||||
m_name = VALUE("name");
|
||||
m_flavour = VALUE("flavour");
|
||||
m_tags = VALUE("tags");
|
||||
const auto catnode = node.child("category");
|
||||
m_category = catnode.empty() ? getCategoryFromTags() : catnode.child_value();
|
||||
m_articleCount = strtoull(VALUE("articleCount"), 0, 0);
|
||||
m_mediaCount = strtoull(VALUE("mediaCount"), 0, 0);
|
||||
for(auto linkNode = node.child("link"); linkNode;
|
||||
linkNode = linkNode.next_sibling("link")) {
|
||||
std::string rel = linkNode.attribute("rel").value();
|
||||
@@ -197,4 +218,29 @@ const std::string& Book::getFavicon() const {
|
||||
return m_favicon;
|
||||
}
|
||||
|
||||
std::string Book::getTagStr(const std::string& tagName) const {
|
||||
return getTagValueFromTagList(convertTags(m_tags), tagName);
|
||||
}
|
||||
|
||||
bool Book::getTagBool(const std::string& tagName) const {
|
||||
return convertStrToBool(getTagStr(tagName));
|
||||
}
|
||||
|
||||
std::string Book::getCategory() const
|
||||
{
|
||||
return m_category;
|
||||
}
|
||||
|
||||
std::string Book::getCategoryFromTags() const
|
||||
{
|
||||
try
|
||||
{
|
||||
return getTagStr("category");
|
||||
}
|
||||
catch ( const std::out_of_range& )
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ Downloader::Downloader() :
|
||||
m_knownDownloads[gid]->updateStatus();
|
||||
}
|
||||
} catch (std::exception& e) {
|
||||
std::cerr << "aria2 tellActive failed : " << e.what();
|
||||
std::cerr << "aria2 tellActive failed : " << e.what() << std::endl;
|
||||
}
|
||||
try {
|
||||
for (auto gid : mp_aria->tellWaiting()) {
|
||||
@@ -141,7 +141,7 @@ Downloader::Downloader() :
|
||||
m_knownDownloads[gid]->updateStatus();
|
||||
}
|
||||
} catch (std::exception& e) {
|
||||
std::cerr << "aria2 tellWaiting failed : " << e.what();
|
||||
std::cerr << "aria2 tellWaiting failed : " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,12 +163,7 @@ std::vector<std::string> Downloader::getDownloadIds() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const std::string &Downloader::getAria2LaunchCmd()
|
||||
{
|
||||
return mp_aria->getLaunchCmd();
|
||||
}
|
||||
|
||||
Download* Downloader::startDownload(const std::string& uri)
|
||||
Download* Downloader::startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options)
|
||||
{
|
||||
for (auto& p: m_knownDownloads) {
|
||||
auto& d = p.second;
|
||||
@@ -177,7 +172,7 @@ Download* Downloader::startDownload(const std::string& uri)
|
||||
return d.get();
|
||||
}
|
||||
std::vector<std::string> uris = {uri};
|
||||
auto gid = mp_aria->addUri(uris);
|
||||
auto gid = mp_aria->addUri(uris, options);
|
||||
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
|
||||
return m_knownDownloads[gid].get();
|
||||
}
|
||||
|
||||
101
src/entry.cpp
101
src/entry.cpp
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2011 Emmanuel Engelhart <kelson@kiwix.org>
|
||||
* Copyright 2018-2020 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,121 +20,54 @@
|
||||
#include "reader.h"
|
||||
#include <time.h>
|
||||
|
||||
#include <zim/search.h>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
Entry::Entry(zim::Article article)
|
||||
: article(article)
|
||||
Entry::Entry(zim::Entry entry)
|
||||
: entry(entry)
|
||||
{
|
||||
}
|
||||
|
||||
#define RETURN_IF_INVALID(WHAT) if(!good()) { return (WHAT); }
|
||||
|
||||
std::string Entry::getPath() const
|
||||
{
|
||||
RETURN_IF_INVALID("");
|
||||
return article.getLongUrl();
|
||||
}
|
||||
|
||||
std::string Entry::getTitle() const
|
||||
{
|
||||
RETURN_IF_INVALID("");
|
||||
return article.getTitle();
|
||||
}
|
||||
|
||||
std::string Entry::getContent() const
|
||||
{
|
||||
RETURN_IF_INVALID("");
|
||||
return article.getData();
|
||||
}
|
||||
|
||||
zim::Blob Entry::getBlob(offset_type offset) const
|
||||
{
|
||||
RETURN_IF_INVALID(zim::Blob());
|
||||
return article.getData(offset);
|
||||
}
|
||||
|
||||
zim::Blob Entry::getBlob(offset_type offset, size_type size) const
|
||||
{
|
||||
RETURN_IF_INVALID(zim::Blob());
|
||||
return article.getData(offset, size);
|
||||
}
|
||||
|
||||
std::pair<std::string, offset_type> Entry::getDirectAccessInfo() const
|
||||
{
|
||||
RETURN_IF_INVALID(std::make_pair("", 0));
|
||||
return article.getDirectAccessInformation();
|
||||
}
|
||||
|
||||
size_type Entry::getSize() const
|
||||
{
|
||||
RETURN_IF_INVALID(0);
|
||||
return article.getArticleSize();
|
||||
if (entry.isRedirect()) {
|
||||
return 0;
|
||||
} else {
|
||||
return entry.getItem().getSize();
|
||||
}
|
||||
}
|
||||
|
||||
std::string Entry::getMimetype() const
|
||||
{
|
||||
RETURN_IF_INVALID("");
|
||||
try {
|
||||
return article.getMimeType();
|
||||
} catch (exception& e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
return entry.getItem(true).getMimetype();
|
||||
}
|
||||
|
||||
bool Entry::isRedirect() const
|
||||
{
|
||||
RETURN_IF_INVALID(false);
|
||||
return article.isRedirect();
|
||||
}
|
||||
|
||||
bool Entry::isLinkTarget() const
|
||||
{
|
||||
RETURN_IF_INVALID(false);
|
||||
return article.isLinktarget();
|
||||
}
|
||||
|
||||
bool Entry::isDeleted() const
|
||||
{
|
||||
RETURN_IF_INVALID(false);
|
||||
return article.isDeleted();
|
||||
return entry.isRedirect();
|
||||
}
|
||||
|
||||
Entry Entry::getRedirectEntry() const
|
||||
{
|
||||
RETURN_IF_INVALID(Entry());
|
||||
if ( !article.isRedirect() ) {
|
||||
if ( !entry.isRedirect() ) {
|
||||
throw NoEntry();
|
||||
}
|
||||
|
||||
auto targeted_article = article.getRedirectArticle();
|
||||
if ( !targeted_article.good()) {
|
||||
throw NoEntry();
|
||||
}
|
||||
return targeted_article;
|
||||
return entry.getRedirectEntry();
|
||||
}
|
||||
|
||||
Entry Entry::getFinalEntry() const
|
||||
{
|
||||
RETURN_IF_INVALID(Entry());
|
||||
if (final_article.good()) {
|
||||
return final_article;
|
||||
}
|
||||
int loopCounter = 42;
|
||||
final_article = article;
|
||||
while (final_article.isRedirect() && loopCounter--) {
|
||||
final_article = final_article.getRedirectArticle();
|
||||
if ( !final_article.good()) {
|
||||
throw NoEntry();
|
||||
}
|
||||
auto final_entry = entry;
|
||||
while (final_entry.isRedirect() && loopCounter--) {
|
||||
final_entry = final_entry.getRedirectEntry();
|
||||
}
|
||||
// Prevent infinite loops.
|
||||
if (final_article.isRedirect()) {
|
||||
if (final_entry.isRedirect()) {
|
||||
throw NoEntry();
|
||||
}
|
||||
return final_article;
|
||||
return final_entry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
410
src/library.cpp
410
src/library.cpp
@@ -30,14 +30,42 @@
|
||||
#include <pugixml.hpp>
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <unicode/locid.h>
|
||||
#include <xapian.h>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string iso639_3ToXapian(const std::string& lang) {
|
||||
return icu::Locale(lang.c_str()).getLanguage();
|
||||
};
|
||||
|
||||
std::string normalizeText(const std::string& text)
|
||||
{
|
||||
return removeAccents(text);
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
class Library::BookDB : public Xapian::WritableDatabase
|
||||
{
|
||||
public:
|
||||
BookDB() : Xapian::WritableDatabase("", Xapian::DB_BACKEND_INMEMORY) {}
|
||||
};
|
||||
|
||||
/* Constructor */
|
||||
Library::Library()
|
||||
: m_bookDB(new BookDB)
|
||||
{
|
||||
}
|
||||
|
||||
Library::Library(Library&& ) = default;
|
||||
|
||||
Library& Library::operator=(Library&& ) = default;
|
||||
|
||||
/* Destructor */
|
||||
Library::~Library()
|
||||
{
|
||||
@@ -47,6 +75,7 @@ Library::~Library()
|
||||
bool Library::addBook(const Book& book)
|
||||
{
|
||||
/* Try to find it */
|
||||
updateBookDB(book);
|
||||
try {
|
||||
auto& oldbook = m_books.at(book.getId());
|
||||
oldbook.update(book);
|
||||
@@ -77,12 +106,38 @@ bool Library::removeBookmark(const std::string& zimId, const std::string& url)
|
||||
|
||||
bool Library::removeBookById(const std::string& id)
|
||||
{
|
||||
m_bookDB->delete_document("Q" + id);
|
||||
m_readers.erase(id);
|
||||
return m_books.erase(id) == 1;
|
||||
}
|
||||
|
||||
const Book& Library::getBookById(const std::string& id) const
|
||||
{
|
||||
return m_books.at(id);
|
||||
}
|
||||
|
||||
Book& Library::getBookById(const std::string& id)
|
||||
{
|
||||
return m_books.at(id);
|
||||
const Library& const_self = *this;
|
||||
return const_cast<Book&>(const_self.getBookById(id));
|
||||
}
|
||||
|
||||
const Book& Library::getBookByPath(const std::string& path) const
|
||||
{
|
||||
for(auto& it: m_books) {
|
||||
auto& book = it.second;
|
||||
if (book.getPath() == path)
|
||||
return book;
|
||||
}
|
||||
std::ostringstream ss;
|
||||
ss << "No book with path " << path << " in the library." << std::endl;
|
||||
throw std::out_of_range(ss.str());
|
||||
}
|
||||
|
||||
Book& Library::getBookByPath(const std::string& path)
|
||||
{
|
||||
const Library& const_self = *this;
|
||||
return const_cast<Book&>(const_self.getBookByPath(path));
|
||||
}
|
||||
|
||||
std::shared_ptr<Reader> Library::getReaderById(const std::string& id)
|
||||
@@ -100,7 +155,7 @@ std::shared_ptr<Reader> Library::getReaderById(const std::string& id)
|
||||
}
|
||||
|
||||
unsigned int Library::getBookCount(const bool localBooks,
|
||||
const bool remoteBooks)
|
||||
const bool remoteBooks) const
|
||||
{
|
||||
unsigned int result = 0;
|
||||
for (auto& pair: m_books) {
|
||||
@@ -113,7 +168,7 @@ unsigned int Library::getBookCount(const bool localBooks,
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Library::writeToFile(const std::string& path)
|
||||
bool Library::writeToFile(const std::string& path) const
|
||||
{
|
||||
auto baseDir = removeLastPathElement(path);
|
||||
LibXMLDumper dumper(this);
|
||||
@@ -121,13 +176,13 @@ bool Library::writeToFile(const std::string& path)
|
||||
return writeTextFile(path, dumper.dumpLibXMLContent(getBooksIds()));
|
||||
}
|
||||
|
||||
bool Library::writeBookmarksToFile(const std::string& path)
|
||||
bool Library::writeBookmarksToFile(const std::string& path) const
|
||||
{
|
||||
LibXMLDumper dumper(this);
|
||||
return writeTextFile(path, dumper.dumpLibXMLBookmark());
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::getBooksLanguages()
|
||||
std::vector<std::string> Library::getBooksLanguages() const
|
||||
{
|
||||
std::vector<std::string> booksLanguages;
|
||||
std::map<std::string, bool> booksLanguagesMap;
|
||||
@@ -146,7 +201,22 @@ std::vector<std::string> Library::getBooksLanguages()
|
||||
return booksLanguages;
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::getBooksCreators()
|
||||
std::vector<std::string> Library::getBooksCategories() const
|
||||
{
|
||||
std::set<std::string> categories;
|
||||
|
||||
for (const auto& pair: m_books) {
|
||||
const auto& book = pair.second;
|
||||
const auto& c = book.getCategory();
|
||||
if ( !c.empty() ) {
|
||||
categories.insert(c);
|
||||
}
|
||||
}
|
||||
|
||||
return std::vector<std::string>(categories.begin(), categories.end());
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::getBooksCreators() const
|
||||
{
|
||||
std::vector<std::string> booksCreators;
|
||||
std::map<std::string, bool> booksCreatorsMap;
|
||||
@@ -165,7 +235,7 @@ std::vector<std::string> Library::getBooksCreators()
|
||||
return booksCreators;
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::getBooksPublishers()
|
||||
std::vector<std::string> Library::getBooksPublishers() const
|
||||
{
|
||||
std::vector<std::string> booksPublishers;
|
||||
std::map<std::string, bool> booksPublishersMap;
|
||||
@@ -184,7 +254,7 @@ std::vector<std::string> Library::getBooksPublishers()
|
||||
return booksPublishers;
|
||||
}
|
||||
|
||||
const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks)
|
||||
const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks) const
|
||||
{
|
||||
if (!onlyValidBookmarks) {
|
||||
return m_bookmarks;
|
||||
@@ -199,9 +269,9 @@ const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks
|
||||
return validBookmarks;
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::getBooksIds()
|
||||
Library::BookIdCollection Library::getBooksIds() const
|
||||
{
|
||||
std::vector<std::string> bookIds;
|
||||
BookIdCollection bookIds;
|
||||
|
||||
for (auto& pair: m_books) {
|
||||
bookIds.push_back(pair.first);
|
||||
@@ -210,7 +280,7 @@ std::vector<std::string> Library::getBooksIds()
|
||||
return bookIds;
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::filter(const std::string& search)
|
||||
Library::BookIdCollection Library::filter(const std::string& search) const
|
||||
{
|
||||
if (search.empty()) {
|
||||
return getBooksIds();
|
||||
@@ -220,16 +290,195 @@ std::vector<std::string> Library::filter(const std::string& search)
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::string> Library::filter(const Filter& filter)
|
||||
void Library::updateBookDB(const Book& book)
|
||||
{
|
||||
std::vector<std::string> bookIds;
|
||||
for(auto& pair:m_books) {
|
||||
auto book = pair.second;
|
||||
if(filter.accept(book)) {
|
||||
bookIds.push_back(pair.first);
|
||||
Xapian::Stem stemmer;
|
||||
Xapian::TermGenerator indexer;
|
||||
const std::string lang = book.getLanguage();
|
||||
try {
|
||||
stemmer = Xapian::Stem(iso639_3ToXapian(lang));
|
||||
indexer.set_stemmer(stemmer);
|
||||
indexer.set_stemming_strategy(Xapian::TermGenerator::STEM_SOME);
|
||||
} catch (...) {}
|
||||
Xapian::Document doc;
|
||||
indexer.set_document(doc);
|
||||
|
||||
const std::string title = normalizeText(book.getTitle());
|
||||
const std::string desc = normalizeText(book.getDescription());
|
||||
|
||||
// Index title and description without prefixes for general search
|
||||
indexer.index_text(title);
|
||||
indexer.increase_termpos();
|
||||
indexer.index_text(desc);
|
||||
|
||||
// Index all fields for field-based search
|
||||
indexer.index_text(title, 1, "S");
|
||||
indexer.index_text(desc, 1, "XD");
|
||||
indexer.index_text(lang, 1, "L");
|
||||
indexer.index_text(normalizeText(book.getCreator()), 1, "A");
|
||||
indexer.index_text(normalizeText(book.getPublisher()), 1, "XP");
|
||||
indexer.index_text(normalizeText(book.getName()), 1, "XN");
|
||||
indexer.index_text(normalizeText(book.getCategory()), 1, "XC");
|
||||
|
||||
for ( const auto& tag : split(normalizeText(book.getTags()), ";") )
|
||||
doc.add_boolean_term("XT" + tag);
|
||||
|
||||
const std::string idterm = "Q" + book.getId();
|
||||
doc.add_boolean_term(idterm);
|
||||
|
||||
doc.set_data(book.getId());
|
||||
|
||||
m_bookDB->replace_document(idterm, doc);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool willSelectEverything(const Xapian::Query& query)
|
||||
{
|
||||
return query.get_type() == Xapian::Query::LEAF_MATCH_ALL;
|
||||
}
|
||||
|
||||
|
||||
Xapian::Query buildXapianQueryFromFilterQuery(const Filter& filter)
|
||||
{
|
||||
if ( !filter.hasQuery() || filter.getQuery().empty() ) {
|
||||
// This is a thread-safe way to construct an equivalent of
|
||||
// a Xapian::Query::MatchAll query
|
||||
return Xapian::Query(std::string());
|
||||
}
|
||||
|
||||
Xapian::QueryParser queryParser;
|
||||
queryParser.set_default_op(Xapian::Query::OP_AND);
|
||||
queryParser.add_prefix("title", "S");
|
||||
queryParser.add_prefix("description", "XD");
|
||||
queryParser.add_prefix("name", "XN");
|
||||
queryParser.add_prefix("category", "XC");
|
||||
queryParser.add_prefix("lang", "L");
|
||||
queryParser.add_prefix("publisher", "XP");
|
||||
queryParser.add_prefix("creator", "A");
|
||||
queryParser.add_prefix("tag", "XT");
|
||||
const auto partialQueryFlag = filter.queryIsPartial()
|
||||
? Xapian::QueryParser::FLAG_PARTIAL
|
||||
: 0;
|
||||
// Language assumed for the query is not known for sure so stemming
|
||||
// is not applied
|
||||
//queryParser.set_stemmer(Xapian::Stem(iso639_3ToXapian(???)));
|
||||
//queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_SOME);
|
||||
const auto flags = Xapian::QueryParser::FLAG_PHRASE
|
||||
| Xapian::QueryParser::FLAG_BOOLEAN
|
||||
| Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE
|
||||
| Xapian::QueryParser::FLAG_LOVEHATE
|
||||
| Xapian::QueryParser::FLAG_WILDCARD
|
||||
| partialQueryFlag;
|
||||
return queryParser.parse_query(normalizeText(filter.getQuery()), flags);
|
||||
}
|
||||
|
||||
Xapian::Query nameQuery(const std::string& name)
|
||||
{
|
||||
return Xapian::Query("XN" + normalizeText(name));
|
||||
}
|
||||
|
||||
Xapian::Query categoryQuery(const std::string& category)
|
||||
{
|
||||
return Xapian::Query("XC" + normalizeText(category));
|
||||
}
|
||||
|
||||
Xapian::Query langQuery(const std::string& lang)
|
||||
{
|
||||
return Xapian::Query("L" + normalizeText(lang));
|
||||
}
|
||||
|
||||
Xapian::Query publisherQuery(const std::string& publisher)
|
||||
{
|
||||
Xapian::QueryParser queryParser;
|
||||
queryParser.set_default_op(Xapian::Query::OP_OR);
|
||||
queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_NONE);
|
||||
const auto flags = 0;
|
||||
const auto q = queryParser.parse_query(normalizeText(publisher), flags, "XP");
|
||||
return Xapian::Query(Xapian::Query::OP_PHRASE, q.get_terms_begin(), q.get_terms_end(), q.get_length());
|
||||
}
|
||||
|
||||
Xapian::Query creatorQuery(const std::string& creator)
|
||||
{
|
||||
Xapian::QueryParser queryParser;
|
||||
queryParser.set_default_op(Xapian::Query::OP_OR);
|
||||
queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_NONE);
|
||||
const auto flags = 0;
|
||||
const auto q = queryParser.parse_query(normalizeText(creator), flags, "A");
|
||||
return Xapian::Query(Xapian::Query::OP_PHRASE, q.get_terms_begin(), q.get_terms_end(), q.get_length());
|
||||
}
|
||||
|
||||
Xapian::Query tagsQuery(const Filter::Tags& acceptTags, const Filter::Tags& rejectTags)
|
||||
{
|
||||
Xapian::Query q = Xapian::Query(std::string());
|
||||
if (!acceptTags.empty()) {
|
||||
for ( const auto& tag : acceptTags )
|
||||
q &= Xapian::Query("XT" + normalizeText(tag));
|
||||
}
|
||||
|
||||
if (!rejectTags.empty()) {
|
||||
for ( const auto& tag : rejectTags )
|
||||
q = Xapian::Query(Xapian::Query::OP_AND_NOT, q, "XT" + normalizeText(tag));
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
Xapian::Query buildXapianQuery(const Filter& filter)
|
||||
{
|
||||
auto q = buildXapianQueryFromFilterQuery(filter);
|
||||
if ( filter.hasName() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, nameQuery(filter.getName()));
|
||||
}
|
||||
if ( filter.hasCategory() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, categoryQuery(filter.getCategory()));
|
||||
}
|
||||
if ( filter.hasLang() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, langQuery(filter.getLang()));
|
||||
}
|
||||
if ( filter.hasPublisher() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, publisherQuery(filter.getPublisher()));
|
||||
}
|
||||
if ( filter.hasCreator() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, creatorQuery(filter.getCreator()));
|
||||
}
|
||||
if ( !filter.getAcceptTags().empty() || !filter.getRejectTags().empty() ) {
|
||||
const auto tq = tagsQuery(filter.getAcceptTags(), filter.getRejectTags());
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, tq);;
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
Library::BookIdCollection Library::filterViaBookDB(const Filter& filter) const
|
||||
{
|
||||
const auto query = buildXapianQuery(filter);
|
||||
|
||||
if ( willSelectEverything(query) )
|
||||
return getBooksIds();
|
||||
|
||||
BookIdCollection bookIds;
|
||||
|
||||
Xapian::Enquire enquire(*m_bookDB);
|
||||
enquire.set_query(query);
|
||||
const auto results = enquire.get_mset(0, m_books.size());
|
||||
for ( auto it = results.begin(); it != results.end(); ++it ) {
|
||||
bookIds.push_back(it.get_document().get_data());
|
||||
}
|
||||
|
||||
return bookIds;
|
||||
}
|
||||
|
||||
Library::BookIdCollection Library::filter(const Filter& filter) const
|
||||
{
|
||||
BookIdCollection result;
|
||||
for(auto id : filterViaBookDB(filter)) {
|
||||
if(filter.accept(m_books.at(id))) {
|
||||
result.push_back(id);
|
||||
}
|
||||
}
|
||||
return bookIds;
|
||||
return result;
|
||||
}
|
||||
|
||||
template<supportedListSortBy SORT>
|
||||
@@ -245,13 +494,13 @@ struct KEY_TYPE<SIZE> {
|
||||
template<supportedListSortBy sort>
|
||||
class Comparator {
|
||||
private:
|
||||
Library* lib;
|
||||
bool ascending;
|
||||
const Library* const lib;
|
||||
const bool ascending;
|
||||
|
||||
inline typename KEY_TYPE<sort>::TYPE get_key(const std::string& id);
|
||||
|
||||
public:
|
||||
Comparator(Library* lib, bool ascending) : lib(lib), ascending(ascending) {}
|
||||
Comparator(const Library* lib, bool ascending) : lib(lib), ascending(ascending) {}
|
||||
inline bool operator() (const std::string& id1, const std::string& id2) {
|
||||
if (ascending) {
|
||||
return get_key(id1) < get_key(id2);
|
||||
@@ -291,7 +540,7 @@ std::string Comparator<PUBLISHER>::get_key(const std::string& id)
|
||||
return lib->getBookById(id).getPublisher();
|
||||
}
|
||||
|
||||
void Library::sort(std::vector<std::string>& bookIds, supportedListSortBy sort, bool ascending)
|
||||
void Library::sort(BookIdCollection& bookIds, supportedListSortBy sort, bool ascending) const
|
||||
{
|
||||
switch(sort) {
|
||||
case TITLE:
|
||||
@@ -315,7 +564,7 @@ void Library::sort(std::vector<std::string>& bookIds, supportedListSortBy sort,
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::string> Library::listBooksIds(
|
||||
Library::BookIdCollection Library::listBooksIds(
|
||||
int mode,
|
||||
supportedListSortBy sortBy,
|
||||
const std::string& search,
|
||||
@@ -323,7 +572,7 @@ std::vector<std::string> Library::listBooksIds(
|
||||
const std::string& creator,
|
||||
const std::string& publisher,
|
||||
const std::vector<std::string>& tags,
|
||||
size_t maxSize) {
|
||||
size_t maxSize) const {
|
||||
|
||||
Filter _filter;
|
||||
if (mode & LOCAL)
|
||||
@@ -378,6 +627,8 @@ enum filterTypes {
|
||||
_CREATOR = FLAG(10),
|
||||
MAXSIZE = FLAG(11),
|
||||
QUERY = FLAG(12),
|
||||
NAME = FLAG(13),
|
||||
CATEGORY = FLAG(14),
|
||||
};
|
||||
|
||||
Filter& Filter::local(bool accept)
|
||||
@@ -416,20 +667,27 @@ Filter& Filter::valid(bool accept)
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::acceptTags(std::vector<std::string> tags)
|
||||
Filter& Filter::acceptTags(const Tags& tags)
|
||||
{
|
||||
_acceptTags = tags;
|
||||
activeFilters |= ACCEPTTAGS;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::rejectTags(std::vector<std::string> tags)
|
||||
Filter& Filter::rejectTags(const Tags& tags)
|
||||
{
|
||||
_rejectTags = tags;
|
||||
activeFilters |= REJECTTAGS;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::category(std::string category)
|
||||
{
|
||||
_category = category;
|
||||
activeFilters |= CATEGORY;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::lang(std::string lang)
|
||||
{
|
||||
_lang = lang;
|
||||
@@ -458,72 +716,70 @@ Filter& Filter::maxSize(size_t maxSize)
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::query(std::string query)
|
||||
Filter& Filter::query(std::string query, bool partial)
|
||||
{
|
||||
_query = query;
|
||||
_queryIsPartial = partial;
|
||||
activeFilters |= QUERY;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::name(std::string name)
|
||||
{
|
||||
_name = name;
|
||||
activeFilters |= NAME;
|
||||
return *this;
|
||||
}
|
||||
|
||||
#define ACTIVE(X) (activeFilters & (X))
|
||||
#define FILTER(TAG, TEST) if (ACTIVE(TAG) && !(TEST)) { return false; }
|
||||
bool Filter::hasQuery() const
|
||||
{
|
||||
return ACTIVE(QUERY);
|
||||
}
|
||||
|
||||
bool Filter::hasName() const
|
||||
{
|
||||
return ACTIVE(NAME);
|
||||
}
|
||||
|
||||
bool Filter::hasCategory() const
|
||||
{
|
||||
return ACTIVE(CATEGORY);
|
||||
}
|
||||
|
||||
bool Filter::hasLang() const
|
||||
{
|
||||
return ACTIVE(LANG);
|
||||
}
|
||||
|
||||
bool Filter::hasPublisher() const
|
||||
{
|
||||
return ACTIVE(_PUBLISHER);
|
||||
}
|
||||
|
||||
bool Filter::hasCreator() const
|
||||
{
|
||||
return ACTIVE(_CREATOR);
|
||||
}
|
||||
|
||||
bool Filter::accept(const Book& book) const
|
||||
{
|
||||
auto local = !book.getPath().empty();
|
||||
if (ACTIVE(_LOCAL) && !local)
|
||||
return false;
|
||||
if (ACTIVE(_NOLOCAL) && local)
|
||||
return false;
|
||||
FILTER(_LOCAL, local)
|
||||
FILTER(_NOLOCAL, !local)
|
||||
|
||||
auto valid = book.isPathValid();
|
||||
if (ACTIVE(_VALID) && !valid)
|
||||
return false;
|
||||
if (ACTIVE(_NOVALID) && valid)
|
||||
return false;
|
||||
FILTER(_VALID, valid)
|
||||
FILTER(_NOVALID, !valid)
|
||||
|
||||
auto remote = !book.getUrl().empty();
|
||||
if (ACTIVE(_REMOTE) && !remote)
|
||||
return false;
|
||||
if (ACTIVE(_NOREMOTE) && remote)
|
||||
return false;
|
||||
if (ACTIVE(ACCEPTTAGS)) {
|
||||
if (!_acceptTags.empty()) {
|
||||
auto vBookTags = split(book.getTags(), ";");
|
||||
std::set<std::string> sBookTags(vBookTags.begin(), vBookTags.end());
|
||||
for (auto& t: _acceptTags) {
|
||||
if (sBookTags.find(t) == sBookTags.end()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ACTIVE(REJECTTAGS)) {
|
||||
if (!_rejectTags.empty()) {
|
||||
auto vBookTags = split(book.getTags(), ";");
|
||||
std::set<std::string> sBookTags(vBookTags.begin(), vBookTags.end());
|
||||
for (auto& t: _rejectTags) {
|
||||
if (sBookTags.find(t) != sBookTags.end()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ACTIVE(MAXSIZE) && book.getSize() > _maxSize)
|
||||
return false;
|
||||
FILTER(_REMOTE, remote)
|
||||
FILTER(_NOREMOTE, !remote)
|
||||
|
||||
if (ACTIVE(LANG) && book.getLanguage() != _lang)
|
||||
return false;
|
||||
|
||||
if (ACTIVE(_PUBLISHER) && book.getPublisher() != _publisher)
|
||||
return false;
|
||||
|
||||
if (ACTIVE(_CREATOR) && book.getCreator() != _creator)
|
||||
return false;
|
||||
|
||||
if ( ACTIVE(QUERY)
|
||||
&& !(matchRegex(book.getTitle(), "\\Q" + _query + "\\E")
|
||||
|| matchRegex(book.getDescription(), "\\Q" + _query + "\\E")))
|
||||
return false;
|
||||
FILTER(MAXSIZE, book.getSize() <= _maxSize)
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
namespace kiwix
|
||||
{
|
||||
/* Constructor */
|
||||
LibXMLDumper::LibXMLDumper(Library* library)
|
||||
LibXMLDumper::LibXMLDumper(const Library* library)
|
||||
: library(library)
|
||||
{
|
||||
}
|
||||
@@ -53,13 +53,15 @@ void LibXMLDumper::handleBook(Book book, pugi::xml_node root_node) {
|
||||
|
||||
if (book.getOrigId().empty()) {
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "title", book.getTitle());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "name", book.getName());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "tags", book.getTags());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "description", book.getDescription());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "language", book.getLanguage());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "creator", book.getCreator());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "publisher", book.getPublisher());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "name", book.getName());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "flavour", book.getFlavour());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "tags", book.getTags());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "faviconMimeType", book.getFaviconMimeType());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "faviconUrl", book.getFaviconUrl());
|
||||
if (!book.getFavicon().empty())
|
||||
ADD_ATTRIBUTE(entry_node, "favicon", base64_encode(book.getFavicon()));
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "manager.h"
|
||||
|
||||
#include "tools/pathTools.h"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace kiwix
|
||||
@@ -46,8 +48,9 @@ Manager::~Manager()
|
||||
}
|
||||
}
|
||||
bool Manager::parseXmlDom(const pugi::xml_document& doc,
|
||||
const bool readOnly,
|
||||
const std::string& libraryPath)
|
||||
bool readOnly,
|
||||
const std::string& libraryPath,
|
||||
bool trustLibrary)
|
||||
{
|
||||
pugi::xml_node libraryNode = doc.child("library");
|
||||
|
||||
@@ -61,12 +64,8 @@ bool Manager::parseXmlDom(const pugi::xml_document& doc,
|
||||
book.updateFromXml(bookNode,
|
||||
removeLastPathElement(libraryPath));
|
||||
|
||||
/* Update the book properties with the new importer */
|
||||
if (libraryVersion.empty()
|
||||
|| atoi(libraryVersion.c_str()) <= atoi(KIWIX_LIBRARY_VERSION)) {
|
||||
if (!book.getPath().empty()) {
|
||||
this->readBookFromPath(book.getPath(), &book);
|
||||
}
|
||||
if (!trustLibrary && !book.getPath().empty()) {
|
||||
this->readBookFromPath(book.getPath(), &book);
|
||||
}
|
||||
manipulator->addBookToLibrary(book);
|
||||
}
|
||||
@@ -75,15 +74,16 @@ bool Manager::parseXmlDom(const pugi::xml_document& doc,
|
||||
}
|
||||
|
||||
bool Manager::readXml(const std::string& xml,
|
||||
const bool readOnly,
|
||||
const std::string& libraryPath)
|
||||
bool readOnly,
|
||||
const std::string& libraryPath,
|
||||
bool trustLibrary)
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result
|
||||
= doc.load_buffer_inplace((void*)xml.data(), xml.size());
|
||||
= doc.load_buffer((void*)xml.data(), xml.size());
|
||||
|
||||
if (result) {
|
||||
this->parseXmlDom(doc, readOnly, libraryPath);
|
||||
this->parseXmlDom(doc, readOnly, libraryPath, trustLibrary);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -124,7 +124,7 @@ bool Manager::readOpds(const std::string& content, const std::string& urlHost)
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result
|
||||
= doc.load_buffer_inplace((void*)content.data(), content.size());
|
||||
= doc.load_buffer((void*)content.data(), content.size());
|
||||
|
||||
if (result) {
|
||||
this->parseOpdsDom(doc, urlHost);
|
||||
@@ -134,21 +134,22 @@ bool Manager::readOpds(const std::string& content, const std::string& urlHost)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Manager::readFile(const std::string& path, const bool readOnly)
|
||||
{
|
||||
return this->readFile(path, path, readOnly);
|
||||
}
|
||||
|
||||
bool Manager::readFile(const std::string& nativePath,
|
||||
const std::string& UTF8Path,
|
||||
const bool readOnly)
|
||||
bool Manager::readFile(
|
||||
const std::string& path,
|
||||
bool readOnly,
|
||||
bool trustLibrary)
|
||||
{
|
||||
bool retVal = true;
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file(nativePath.c_str());
|
||||
|
||||
#ifdef _WIN32
|
||||
pugi::xml_parse_result result = doc.load_file(Utf8ToWide(path).c_str());
|
||||
#else
|
||||
pugi::xml_parse_result result = doc.load_file(path.c_str());
|
||||
#endif
|
||||
|
||||
if (result) {
|
||||
this->parseXmlDom(doc, readOnly, UTF8Path);
|
||||
this->parseXmlDom(doc, readOnly, path, trustLibrary);
|
||||
} else {
|
||||
retVal = false;
|
||||
}
|
||||
@@ -157,7 +158,7 @@ bool Manager::readFile(const std::string& nativePath,
|
||||
* able to know where to save the library if new content are
|
||||
* available */
|
||||
if (!readOnly) {
|
||||
this->writableLibraryPath = UTF8Path;
|
||||
this->writableLibraryPath = path;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
@@ -174,7 +175,7 @@ std::string Manager::addBookFromPathAndGetId(const std::string& pathToOpen,
|
||||
kiwix::Book book;
|
||||
|
||||
if (this->readBookFromPath(pathToOpen, &book)) {
|
||||
if (pathToSave != pathToOpen) {
|
||||
if (!pathToSave.empty() && pathToSave != pathToOpen) {
|
||||
book.setPath(isRelativePath(pathToSave)
|
||||
? computeAbsolutePath(
|
||||
removeLastPathElement(writableLibraryPath),
|
||||
|
||||
@@ -21,8 +21,12 @@ kiwix_sources = [
|
||||
'tools/otherTools.cpp',
|
||||
'kiwixserve.cpp',
|
||||
'name_mapper.cpp',
|
||||
'server/byte_range.cpp',
|
||||
'server/etag.cpp',
|
||||
'server/request_context.cpp',
|
||||
'server/response.cpp'
|
||||
'server/response.cpp',
|
||||
'server/internalServer.cpp',
|
||||
'server/internalServer_catalog_v2.cpp'
|
||||
]
|
||||
kiwix_sources += lib_resources
|
||||
|
||||
@@ -32,13 +36,16 @@ else
|
||||
kiwix_sources += 'subprocess_unix.cpp'
|
||||
endif
|
||||
|
||||
if get_option('android')
|
||||
subdir('android')
|
||||
if wrapper.contains('android')
|
||||
install_dir = 'kiwix-lib/jniLibs/' + meson.get_cross_property('android_abi')
|
||||
else
|
||||
install_dir = get_option('libdir')
|
||||
endif
|
||||
|
||||
if wrapper.contains('android') or wrapper.contains('java')
|
||||
subdir('wrapper/java')
|
||||
endif
|
||||
|
||||
config_h = configure_file(output : 'kiwix_config.h',
|
||||
configuration : conf,
|
||||
input : 'config.h.in')
|
||||
@@ -48,6 +55,7 @@ kiwixlib = library('kiwix',
|
||||
kiwix_sources,
|
||||
include_directories : inc,
|
||||
dependencies : all_deps,
|
||||
link_args: extra_libs,
|
||||
version: meson.project_version(),
|
||||
install: true,
|
||||
install_dir: install_dir)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Emmanuel Engelhart <kelson@kiwix.org>
|
||||
* Copyright (C) 2017 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
* Copyright 2020 Emmanuel Engelhart <kelson@kiwix.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -18,21 +17,8 @@
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixlib;
|
||||
#include <microhttpd.h>
|
||||
|
||||
import org.kiwix.kiwixlib.JNIKiwixException;
|
||||
|
||||
public class JNIKiwixLibrary
|
||||
{
|
||||
public native boolean addBook(String path) throws JNIKiwixException;
|
||||
|
||||
public JNIKiwixLibrary()
|
||||
{
|
||||
nativeHandle = getNativeLibrary();
|
||||
}
|
||||
|
||||
public native void dispose();
|
||||
|
||||
private native long getNativeLibrary();
|
||||
private long nativeHandle;
|
||||
}
|
||||
#if MHD_VERSION < 0x00097002
|
||||
typedef int MHD_Result;
|
||||
#endif
|
||||
@@ -21,10 +21,13 @@
|
||||
#include "book.h"
|
||||
|
||||
#include "tools/otherTools.h"
|
||||
#include <iomanip>
|
||||
|
||||
#include "kiwixlib-resources.h"
|
||||
#include <mustache.hpp>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
/* Constructor */
|
||||
OPDSDumper::OPDSDumper(Library* library)
|
||||
: library(library)
|
||||
@@ -35,112 +38,111 @@ OPDSDumper::~OPDSDumper()
|
||||
{
|
||||
}
|
||||
|
||||
std::string gen_date_str()
|
||||
{
|
||||
auto now = time(0);
|
||||
auto tm = localtime(&now);
|
||||
|
||||
std::stringstream is;
|
||||
is << std::setw(2) << std::setfill('0')
|
||||
<< 1900+tm->tm_year << "-"
|
||||
<< std::setw(2) << std::setfill('0') << tm->tm_mon << "-"
|
||||
<< std::setw(2) << std::setfill('0') << tm->tm_mday << "T"
|
||||
<< std::setw(2) << std::setfill('0') << tm->tm_hour << ":"
|
||||
<< std::setw(2) << std::setfill('0') << tm->tm_min << ":"
|
||||
<< std::setw(2) << std::setfill('0') << tm->tm_sec << "Z";
|
||||
return is.str();
|
||||
}
|
||||
|
||||
static std::string gen_date_from_yyyy_mm_dd(const std::string& date)
|
||||
{
|
||||
std::stringstream is;
|
||||
is << date << "T00:00::00:Z";
|
||||
return is.str();
|
||||
}
|
||||
|
||||
void OPDSDumper::setOpenSearchInfo(int totalResults, int startIndex, int count)
|
||||
{
|
||||
m_totalResults = totalResults;
|
||||
m_startIndex = startIndex,
|
||||
m_count = count;
|
||||
m_isSearchResult = true;
|
||||
}
|
||||
|
||||
#define ADD_TEXT_ENTRY(node, child, value) (node).append_child((child)).append_child(pugi::node_pcdata).set_value((value).c_str())
|
||||
|
||||
pugi::xml_node OPDSDumper::handleBook(Book book, pugi::xml_node root_node) {
|
||||
auto entry_node = root_node.append_child("entry");
|
||||
ADD_TEXT_ENTRY(entry_node, "title", book.getTitle());
|
||||
ADD_TEXT_ENTRY(entry_node, "id", "urn:uuid:"+book.getId());
|
||||
ADD_TEXT_ENTRY(entry_node, "icon", rootLocation + "/meta?name=favicon&content=" + book.getHumanReadableIdFromPath());
|
||||
ADD_TEXT_ENTRY(entry_node, "updated", gen_date_from_yyyy_mm_dd(book.getDate()));
|
||||
ADD_TEXT_ENTRY(entry_node, "summary", book.getDescription());
|
||||
ADD_TEXT_ENTRY(entry_node, "tags", book.getTags());
|
||||
|
||||
auto content_node = entry_node.append_child("link");
|
||||
content_node.append_attribute("type") = "text/html";
|
||||
content_node.append_attribute("href") = (rootLocation + "/" + book.getHumanReadableIdFromPath()).c_str();
|
||||
|
||||
auto author_node = entry_node.append_child("author");
|
||||
ADD_TEXT_ENTRY(author_node, "name", book.getCreator());
|
||||
|
||||
if (! book.getUrl().empty()) {
|
||||
auto acquisition_link = entry_node.append_child("link");
|
||||
acquisition_link.append_attribute("rel") = "http://opds-spec.org/acquisition/open-access";
|
||||
acquisition_link.append_attribute("type") = "application/x-zim";
|
||||
acquisition_link.append_attribute("href") = book.getUrl().c_str();
|
||||
acquisition_link.append_attribute("length") = to_string(book.getSize()).c_str();
|
||||
}
|
||||
|
||||
if (! book.getFaviconMimeType().empty() ) {
|
||||
auto image_link = entry_node.append_child("link");
|
||||
image_link.append_attribute("rel") = "http://opds-spec.org/image/thumbnail";
|
||||
image_link.append_attribute("type") = book.getFaviconMimeType().c_str();
|
||||
image_link.append_attribute("href") = (rootLocation + "/meta?name=favicon&content=" + book.getHumanReadableIdFromPath()).c_str();
|
||||
}
|
||||
return entry_node;
|
||||
}
|
||||
|
||||
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds)
|
||||
namespace
|
||||
{
|
||||
date = gen_date_str();
|
||||
pugi::xml_document doc;
|
||||
|
||||
auto root_node = doc.append_child("feed");
|
||||
root_node.append_attribute("xmlns") = "http://www.w3.org/2005/Atom";
|
||||
root_node.append_attribute("xmlns:opds") = "http://opds-spec.org/2010/catalog";
|
||||
typedef kainjow::mustache::data MustacheData;
|
||||
typedef kainjow::mustache::list BookData;
|
||||
|
||||
ADD_TEXT_ENTRY(root_node, "id", id);
|
||||
|
||||
ADD_TEXT_ENTRY(root_node, "title", title);
|
||||
ADD_TEXT_ENTRY(root_node, "updated", date);
|
||||
|
||||
if (m_isSearchResult) {
|
||||
ADD_TEXT_ENTRY(root_node, "totalResults", to_string(m_totalResults));
|
||||
ADD_TEXT_ENTRY(root_node, "startIndex", to_string(m_startIndex));
|
||||
ADD_TEXT_ENTRY(root_node, "itemsPerPage", to_string(m_count));
|
||||
BookData getBookData(const Library* library, const std::vector<std::string>& bookIds)
|
||||
{
|
||||
BookData bookData;
|
||||
for ( const auto& bookId : bookIds ) {
|
||||
const Book& book = library->getBookById(bookId);
|
||||
const MustacheData bookUrl = book.getUrl().empty()
|
||||
? MustacheData(false)
|
||||
: MustacheData(book.getUrl());
|
||||
bookData.push_back(kainjow::mustache::object{
|
||||
{"id", "urn:uuid:"+book.getId()},
|
||||
{"name", book.getName()},
|
||||
{"title", book.getTitle()},
|
||||
{"description", book.getDescription()},
|
||||
{"language", book.getLanguage()},
|
||||
{"content_id", book.getHumanReadableIdFromPath()},
|
||||
{"updated", book.getDate() + "T00:00:00Z"},
|
||||
{"category", book.getCategory()},
|
||||
{"flavour", book.getFlavour()},
|
||||
{"tags", book.getTags()},
|
||||
{"article_count", to_string(book.getArticleCount())},
|
||||
{"media_count", to_string(book.getMediaCount())},
|
||||
{"author_name", book.getCreator()},
|
||||
{"publisher_name", book.getPublisher()},
|
||||
{"url", bookUrl},
|
||||
{"size", to_string(book.getSize())},
|
||||
});
|
||||
}
|
||||
|
||||
auto self_link_node = root_node.append_child("link");
|
||||
self_link_node.append_attribute("rel") = "self";
|
||||
self_link_node.append_attribute("href") = "";
|
||||
self_link_node.append_attribute("type") = "application/atom+xml";
|
||||
return bookData;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
if (!searchDescriptionUrl.empty() ) {
|
||||
auto search_link = root_node.append_child("link");
|
||||
search_link.append_attribute("rel") = "search";
|
||||
search_link.append_attribute("type") = "application/opensearchdescription+xml";
|
||||
search_link.append_attribute("href") = searchDescriptionUrl.c_str();
|
||||
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
|
||||
{
|
||||
const auto bookData = getBookData(library, bookIds);
|
||||
const kainjow::mustache::object template_data{
|
||||
{"date", gen_date_str()},
|
||||
{"root", rootLocation},
|
||||
{"feed_id", gen_uuid(libraryId + "/catalog/search?"+query)},
|
||||
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
|
||||
{"totalResults", to_string(m_totalResults)},
|
||||
{"startIndex", to_string(m_startIndex)},
|
||||
{"itemsPerPage", to_string(m_count)},
|
||||
{"books", bookData }
|
||||
};
|
||||
|
||||
return render_template(RESOURCE::templates::catalog_entries_xml, template_data);
|
||||
}
|
||||
|
||||
string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query) const
|
||||
{
|
||||
const auto bookData = getBookData(library, bookIds);
|
||||
|
||||
const kainjow::mustache::object template_data{
|
||||
{"date", gen_date_str()},
|
||||
{"endpoint_root", rootLocation + "/catalog/v2"},
|
||||
{"feed_id", gen_uuid(libraryId + "/entries?"+query)},
|
||||
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
|
||||
{"query", query.empty() ? "" : "?" + urlEncode(query)},
|
||||
{"totalResults", to_string(m_totalResults)},
|
||||
{"startIndex", to_string(m_startIndex)},
|
||||
{"itemsPerPage", to_string(m_count)},
|
||||
{"books", bookData }
|
||||
};
|
||||
|
||||
return render_template(RESOURCE::templates::catalog_v2_entries_xml, template_data);
|
||||
}
|
||||
|
||||
std::string OPDSDumper::categoriesOPDSFeed(const std::vector<std::string>& categories) const
|
||||
{
|
||||
const auto now = gen_date_str();
|
||||
kainjow::mustache::list categoryData;
|
||||
for ( const auto& category : categories ) {
|
||||
const auto urlencodedCategoryName = urlEncode(category);
|
||||
categoryData.push_back(kainjow::mustache::object{
|
||||
{"name", category},
|
||||
{"urlencoded_name", urlencodedCategoryName},
|
||||
{"updated", now},
|
||||
{"id", gen_uuid(libraryId + "/categories/" + urlencodedCategoryName)}
|
||||
});
|
||||
}
|
||||
|
||||
if (library) {
|
||||
for (auto& bookId: bookIds) {
|
||||
handleBook(library->getBookById(bookId), root_node);
|
||||
}
|
||||
}
|
||||
|
||||
return nodeToString(root_node);
|
||||
return render_template(
|
||||
RESOURCE::templates::catalog_v2_categories_xml,
|
||||
kainjow::mustache::object{
|
||||
{"date", now},
|
||||
{"endpoint_root", rootLocation + "/catalog/v2"},
|
||||
{"feed_id", gen_uuid(libraryId + "/categories")},
|
||||
{"categories", categoryData }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
672
src/reader.cpp
672
src/reader.cpp
@@ -21,6 +21,10 @@
|
||||
#include <time.h>
|
||||
|
||||
#include <zim/search.h>
|
||||
#include <zim/item.h>
|
||||
#include <zim/error.h>
|
||||
|
||||
#include "tools/otherTools.h"
|
||||
|
||||
inline char hi(char v)
|
||||
{
|
||||
@@ -63,7 +67,9 @@ std::string hexUUID(std::string in)
|
||||
namespace kiwix
|
||||
{
|
||||
/* Constructor */
|
||||
Reader::Reader(const string zimFilePath) : zimFileHandler(NULL)
|
||||
Reader::Reader(const string zimFilePath)
|
||||
: zimArchive(nullptr),
|
||||
zimFilePath(zimFilePath)
|
||||
{
|
||||
string tmpZimFilePath = zimFilePath;
|
||||
|
||||
@@ -74,58 +80,45 @@ Reader::Reader(const string zimFilePath) : zimFileHandler(NULL)
|
||||
tmpZimFilePath.resize(tmpZimFilePath.size() - 2);
|
||||
}
|
||||
|
||||
this->zimFileHandler = new zim::File(tmpZimFilePath);
|
||||
|
||||
if (this->zimFileHandler != NULL) {
|
||||
this->firstArticleOffset
|
||||
= this->zimFileHandler->getNamespaceBeginOffset('A');
|
||||
this->lastArticleOffset = this->zimFileHandler->getNamespaceEndOffset('A');
|
||||
this->nsACount = this->zimFileHandler->getNamespaceCount('A');
|
||||
this->nsICount = this->zimFileHandler->getNamespaceCount('I');
|
||||
this->zimFilePath = zimFilePath;
|
||||
}
|
||||
zimArchive.reset(new zim::Archive(tmpZimFilePath));
|
||||
|
||||
/* initialize random seed: */
|
||||
srand(time(NULL));
|
||||
srand(time(nullptr));
|
||||
}
|
||||
|
||||
/* Destructor */
|
||||
Reader::~Reader()
|
||||
#ifndef _WIN32
|
||||
Reader::Reader(int fd)
|
||||
: zimArchive(new zim::Archive(fd)),
|
||||
zimFilePath("")
|
||||
{
|
||||
if (this->zimFileHandler != NULL) {
|
||||
delete this->zimFileHandler;
|
||||
/* initialize random seed: */
|
||||
srand(time(nullptr));
|
||||
}
|
||||
|
||||
Reader::Reader(int fd, zim::offset_type offset, zim::size_type size)
|
||||
: zimArchive(new zim::Archive(fd, offset, size)),
|
||||
zimFilePath("")
|
||||
{
|
||||
/* initialize random seed: */
|
||||
srand(time(nullptr));
|
||||
}
|
||||
#endif // #ifndef _WIN32
|
||||
|
||||
zim::Archive* Reader::getZimArchive() const
|
||||
{
|
||||
return zimArchive.get();
|
||||
}
|
||||
|
||||
MimeCounterType Reader::parseCounterMetadata() const
|
||||
{
|
||||
try {
|
||||
auto counterContent = zimArchive->getMetadata("Counter");
|
||||
return parseMimetypeCounter(counterContent);
|
||||
} catch (zim::EntryNotFound& e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
zim::File* Reader::getZimFileHandler() const
|
||||
{
|
||||
return this->zimFileHandler;
|
||||
}
|
||||
std::map<const std::string, unsigned int> Reader::parseCounterMetadata() const
|
||||
{
|
||||
std::map<const std::string, unsigned int> counters;
|
||||
string mimeType, item, counterString;
|
||||
unsigned int counter;
|
||||
|
||||
zim::Article article = this->zimFileHandler->getArticle('M', "Counter");
|
||||
|
||||
if (article.good()) {
|
||||
stringstream ssContent(article.getData());
|
||||
|
||||
while (getline(ssContent, item, ';')) {
|
||||
stringstream ssItem(item);
|
||||
getline(ssItem, mimeType, '=');
|
||||
getline(ssItem, counterString, '=');
|
||||
if (!counterString.empty() && !mimeType.empty()) {
|
||||
sscanf(counterString.c_str(), "%u", &counter);
|
||||
counters.insert(pair<string, int>(mimeType, counter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return counters;
|
||||
}
|
||||
|
||||
/* Get the count of articles which can be indexed/displayed */
|
||||
unsigned int Reader::getArticleCount() const
|
||||
{
|
||||
@@ -133,12 +126,9 @@ unsigned int Reader::getArticleCount() const
|
||||
= this->parseCounterMetadata();
|
||||
unsigned int counter = 0;
|
||||
|
||||
if (counterMap.empty()) {
|
||||
counter = this->nsACount;
|
||||
} else {
|
||||
auto it = counterMap.find("text/html");
|
||||
if (it != counterMap.end()) {
|
||||
counter = it->second;
|
||||
for(auto &pair:counterMap) {
|
||||
if (startsWith(pair.first, "text/html")) {
|
||||
counter += pair.second;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,139 +142,68 @@ unsigned int Reader::getMediaCount() const
|
||||
= this->parseCounterMetadata();
|
||||
unsigned int counter = 0;
|
||||
|
||||
if (counterMap.empty()) {
|
||||
counter = this->nsICount;
|
||||
} else {
|
||||
auto it = counterMap.find("image/jpeg");
|
||||
if (it != counterMap.end()) {
|
||||
counter += it->second;
|
||||
}
|
||||
|
||||
it = counterMap.find("image/gif");
|
||||
if (it != counterMap.end()) {
|
||||
counter += it->second;
|
||||
}
|
||||
|
||||
it = counterMap.find("image/png");
|
||||
if (it != counterMap.end()) {
|
||||
counter += it->second;
|
||||
for (auto &pair:counterMap) {
|
||||
if (startsWith(pair.first, "image/") ||
|
||||
startsWith(pair.first, "video/") ||
|
||||
startsWith(pair.first, "audio/")) {
|
||||
counter += pair.second;
|
||||
}
|
||||
}
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
/* Get the total of all items of a ZIM file, redirects included */
|
||||
unsigned int Reader::getGlobalCount() const
|
||||
{
|
||||
return this->zimFileHandler->getCountArticles();
|
||||
return zimArchive->getEntryCount();
|
||||
}
|
||||
|
||||
/* Return the UID of the ZIM file */
|
||||
string Reader::getId() const
|
||||
{
|
||||
std::ostringstream s;
|
||||
s << this->zimFileHandler->getFileheader().getUuid();
|
||||
s << zimArchive->getUuid();
|
||||
return s.str();
|
||||
}
|
||||
|
||||
/* Return a page url from a title */
|
||||
bool Reader::getPageUrlFromTitle(const string& title, string& url) const
|
||||
{
|
||||
try {
|
||||
auto entry = getEntryFromTitle(title);
|
||||
entry = entry.getFinalEntry();
|
||||
url = entry.getPath();
|
||||
return true;
|
||||
} catch (NoEntry& e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Return an URL from a title */
|
||||
string Reader::getRandomPageUrl() const
|
||||
{
|
||||
return getRandomPage().getPath();
|
||||
}
|
||||
|
||||
Entry Reader::getRandomPage() const
|
||||
{
|
||||
if (!this->zimFileHandler) {
|
||||
try {
|
||||
return zimArchive->getRandomEntry();
|
||||
} catch(...) {
|
||||
throw NoEntry();
|
||||
}
|
||||
|
||||
zim::Article article;
|
||||
std::string mainPagePath = this->getMainPage().getPath();
|
||||
int watchdog = 42;
|
||||
|
||||
do {
|
||||
auto idx = this->firstArticleOffset
|
||||
+ (zim::size_type)((double)rand() / ((double)RAND_MAX + 1)
|
||||
* this->nsACount);
|
||||
article = zimFileHandler->getArticle(idx);
|
||||
if (!watchdog--) {
|
||||
throw NoEntry();
|
||||
}
|
||||
} while (!article.good() && article.getLongUrl() == mainPagePath);
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
/* Return the welcome page URL */
|
||||
string Reader::getMainPageUrl() const
|
||||
{
|
||||
return getMainPage().getPath();
|
||||
}
|
||||
|
||||
Entry Reader::getMainPage() const
|
||||
{
|
||||
if (!this->zimFileHandler) {
|
||||
throw NoEntry();
|
||||
}
|
||||
|
||||
zim::Article article;
|
||||
if (this->zimFileHandler->getFileheader().hasMainPage())
|
||||
{
|
||||
article = zimFileHandler->getArticle(
|
||||
this->zimFileHandler->getFileheader().getMainPage());
|
||||
}
|
||||
|
||||
if (!article.good())
|
||||
{
|
||||
return getFirstPage();
|
||||
}
|
||||
|
||||
return article;
|
||||
return zimArchive->getMainEntry();
|
||||
}
|
||||
|
||||
bool Reader::getFavicon(string& content, string& mimeType) const
|
||||
{
|
||||
static const char* const paths[] = {"-/favicon", "-/favicon.png", "I/favicon.png", "I/favicon"};
|
||||
|
||||
for (auto &path: paths) {
|
||||
try {
|
||||
auto entry = getEntryFromPath(path);
|
||||
entry = entry.getFinalEntry();
|
||||
content = entry.getContent();
|
||||
mimeType = entry.getMimetype();
|
||||
return true;
|
||||
} catch(NoEntry& e) {};
|
||||
}
|
||||
try {
|
||||
auto item = zimArchive->getIllustrationItem();
|
||||
content = item.getData();
|
||||
mimeType = item.getMimetype();
|
||||
return true;
|
||||
} catch(zim::EntryNotFound& e) {};
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string Reader::getZimFilePath() const
|
||||
{
|
||||
return this->zimFilePath;
|
||||
return zimFilePath;
|
||||
}
|
||||
/* Return a metatag value */
|
||||
bool Reader::getMetadata(const string& name, string& value) const
|
||||
{
|
||||
try {
|
||||
auto entry = getEntryFromPath("M/"+name);
|
||||
value = entry.getContent();
|
||||
value = zimArchive->getMetadata(name);
|
||||
return true;
|
||||
} catch(NoEntry& e) {
|
||||
} catch(zim::EntryNotFound& e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -298,10 +217,9 @@ string Reader::getName() const
|
||||
|
||||
string Reader::getTitle() const
|
||||
{
|
||||
string value;
|
||||
this->getMetadata("Title", value);
|
||||
string value = zimArchive->getMetadata("Title");
|
||||
if (value.empty()) {
|
||||
value = getLastPathElement(zimFileHandler->getFilename());
|
||||
value = getLastPathElement(zimFilePath);
|
||||
std::replace(value.begin(), value.end(), '_', ' ');
|
||||
size_t pos = value.find(".zim");
|
||||
value = value.substr(0, pos);
|
||||
@@ -352,43 +270,6 @@ string Reader::getLicense() const
|
||||
METADATA("License")
|
||||
}
|
||||
|
||||
std::vector<std::string> convertTags(const std::string& tags_str)
|
||||
{
|
||||
auto tags = split(tags_str, ";");
|
||||
std::vector<std::string> tagsList;
|
||||
bool picSeen(false), vidSeen(false), detSeen(false), indexSeen(false);
|
||||
for (auto tag: tags) {
|
||||
picSeen |= (tag == "nopic" || startsWith(tag, "_pictures:"));
|
||||
vidSeen |= (tag == "novid" || startsWith(tag, "_videos:"));
|
||||
detSeen |= (tag == "nodet" || startsWith(tag, "_details:"));
|
||||
indexSeen |= startsWith(tag, "_ftindex");
|
||||
if (tag == "nopic") {
|
||||
tagsList.push_back("_pictures:no");
|
||||
} else if (tag == "novid") {
|
||||
tagsList.push_back("_videos:no");
|
||||
} else if (tag == "nodet") {
|
||||
tagsList.push_back("_details:no");
|
||||
} else if (tag == "_ftindex") {
|
||||
tagsList.push_back("_ftindex:yes");
|
||||
} else {
|
||||
tagsList.push_back(tag);
|
||||
}
|
||||
}
|
||||
if (!indexSeen) {
|
||||
tagsList.push_back("_ftindex:no");
|
||||
}
|
||||
if (!picSeen) {
|
||||
tagsList.push_back("_pictures:yes");
|
||||
}
|
||||
if (!vidSeen) {
|
||||
tagsList.push_back("_videos:yes");
|
||||
}
|
||||
if (!detSeen) {
|
||||
tagsList.push_back("_details:yes");
|
||||
}
|
||||
return tagsList;
|
||||
}
|
||||
|
||||
string Reader::getTags(bool original) const
|
||||
{
|
||||
string tags_str;
|
||||
@@ -400,26 +281,6 @@ string Reader::getTags(bool original) const
|
||||
return join(tags, ";");
|
||||
}
|
||||
|
||||
string getTagValueFromTagList(const std::vector<std::string>& tagList, const std::string& tagName)
|
||||
{
|
||||
for (auto tag: tagList) {
|
||||
if (tag[0] == '_') {
|
||||
auto delimPos = tag.find(':');
|
||||
if (delimPos == string::npos) {
|
||||
// No delimiter... what to do ?
|
||||
continue;
|
||||
}
|
||||
auto cTagName = tag.substr(1, delimPos-1);
|
||||
auto cTagValue = tag.substr(delimPos+1);
|
||||
if (cTagName == tagName) {
|
||||
return cTagValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << tagName << " cannot be found";
|
||||
throw std::out_of_range(ss.str());
|
||||
}
|
||||
|
||||
string Reader::getTagStr(const std::string& tagName) const
|
||||
{
|
||||
@@ -430,16 +291,7 @@ string Reader::getTagStr(const std::string& tagName) const
|
||||
|
||||
bool Reader::getTagBool(const std::string& tagName) const
|
||||
{
|
||||
auto tagValue = getTagStr(tagName);
|
||||
if (tagValue == "yes") {
|
||||
return true;
|
||||
} else if (tagValue == "no") {
|
||||
return false;
|
||||
} else {
|
||||
std::stringstream ss;
|
||||
ss << "Tag value '" << tagValue << "' for " << tagName << " cannot be converted to bool.";
|
||||
throw std::domain_error(ss.str());
|
||||
}
|
||||
return convertStrToBool(getTagStr(tagName));
|
||||
}
|
||||
|
||||
string Reader::getRelation() const
|
||||
@@ -488,83 +340,17 @@ string Reader::getOrigId() const
|
||||
return origID;
|
||||
}
|
||||
|
||||
/* Return the first page URL */
|
||||
string Reader::getFirstPageUrl() const
|
||||
{
|
||||
return getFirstPage().getPath();
|
||||
}
|
||||
|
||||
Entry Reader::getFirstPage() const
|
||||
{
|
||||
if (!this->zimFileHandler) {
|
||||
throw NoEntry();
|
||||
}
|
||||
|
||||
auto firstPageOffset = zimFileHandler->getNamespaceBeginOffset('A');
|
||||
auto article = zimFileHandler->getArticle(firstPageOffset);
|
||||
|
||||
if (! article.good()) {
|
||||
throw NoEntry();
|
||||
}
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
bool _parseUrl(const string& url, char* ns, string& title)
|
||||
{
|
||||
/* Offset to visit the url */
|
||||
unsigned int urlLength = url.size();
|
||||
unsigned int offset = 0;
|
||||
|
||||
/* Ignore the first '/' */
|
||||
if (url[offset] == '/')
|
||||
offset++;
|
||||
|
||||
if (url[offset] == '/' || offset >= urlLength)
|
||||
return false;
|
||||
|
||||
/* Get namespace */
|
||||
*ns = url[offset++];
|
||||
|
||||
if (url[offset] != '/' || offset >= urlLength)
|
||||
return false;
|
||||
|
||||
offset++;
|
||||
|
||||
if ( offset >= urlLength)
|
||||
return false;
|
||||
|
||||
/* Get content title */
|
||||
title = url.substr(offset, urlLength - offset);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Reader::parseUrl(const string& url, char* ns, string& title) const
|
||||
{
|
||||
return _parseUrl(url, ns, title);
|
||||
}
|
||||
|
||||
Entry Reader::getEntryFromPath(const std::string& path) const
|
||||
{
|
||||
char ns = 0;
|
||||
std::string short_url;
|
||||
|
||||
if (!this->zimFileHandler) {
|
||||
throw NoEntry();
|
||||
}
|
||||
_parseUrl(path, &ns, short_url);
|
||||
|
||||
if (short_url.empty() && ns == 0) {
|
||||
if (path.empty() || path == "/") {
|
||||
return getMainPage();
|
||||
}
|
||||
|
||||
auto article = zimFileHandler->getArticle(ns, short_url);
|
||||
if (!article.good()) {
|
||||
try {
|
||||
return zimArchive->getEntryByPath(path);
|
||||
} catch (zim::EntryNotFound& e) {
|
||||
throw NoEntry();
|
||||
}
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
Entry Reader::getEntryFromEncodedPath(const std::string& path) const
|
||||
@@ -574,212 +360,30 @@ Entry Reader::getEntryFromEncodedPath(const std::string& path) const
|
||||
|
||||
Entry Reader::getEntryFromTitle(const std::string& title) const
|
||||
{
|
||||
if (!this->zimFileHandler) {
|
||||
try {
|
||||
return zimArchive->getEntryByTitle(title);
|
||||
} catch(zim::EntryNotFound& e) {
|
||||
throw NoEntry();
|
||||
}
|
||||
|
||||
auto article = this->zimFileHandler->getArticleByTitle('A', title);
|
||||
if (!article.good()) {
|
||||
throw NoEntry();
|
||||
}
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
/* Return article by url */
|
||||
bool Reader::getArticleObjectByDecodedUrl(const string& url,
|
||||
zim::Article& article) const
|
||||
{
|
||||
if (this->zimFileHandler == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Parse the url */
|
||||
char ns = 0;
|
||||
string urlStr;
|
||||
_parseUrl(url, &ns, urlStr);
|
||||
|
||||
/* Main page */
|
||||
if (urlStr.empty() && ns == 0) {
|
||||
_parseUrl(this->getMainPage().getPath(), &ns, urlStr);
|
||||
}
|
||||
|
||||
/* Extract the content from the zim file */
|
||||
article = zimFileHandler->getArticle(ns, urlStr);
|
||||
return article.good();
|
||||
}
|
||||
|
||||
/* Return the mimeType without the content */
|
||||
bool Reader::getMimeTypeByUrl(const string& url, string& mimeType) const
|
||||
{
|
||||
try {
|
||||
auto entry = getEntryFromPath(url);
|
||||
mimeType = entry.getMimetype();
|
||||
return true;
|
||||
} catch (NoEntry& e) {
|
||||
mimeType = "";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get_content_by_decoded_url(const Reader& reader,
|
||||
const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType,
|
||||
string& baseUrl)
|
||||
{
|
||||
content = "";
|
||||
contentType = "";
|
||||
contentLength = 0;
|
||||
|
||||
try {
|
||||
auto entry = reader.getEntryFromPath(url);
|
||||
entry = entry.getFinalEntry();
|
||||
baseUrl = entry.getPath();
|
||||
contentType = entry.getMimetype();
|
||||
content = entry.getContent();
|
||||
contentLength = entry.getSize();
|
||||
title = entry.getTitle();
|
||||
|
||||
/* Try to set a stub HTML header/footer if necesssary */
|
||||
if (contentType.find("text/html") != string::npos
|
||||
&& content.find("<body") == std::string::npos
|
||||
&& content.find("<BODY") == std::string::npos) {
|
||||
content = "<html><head><title>" + title +
|
||||
"</title><meta http-equiv=\"Content-Type\" content=\"text/html; "
|
||||
"charset=utf-8\" /></head><body>" +
|
||||
content + "</body></html>";
|
||||
}
|
||||
return true;
|
||||
} catch (NoEntry& e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Get a content from a zim file */
|
||||
bool Reader::getContentByUrl(const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType) const
|
||||
{
|
||||
std::string stubRedirectUrl;
|
||||
return get_content_by_decoded_url(*this,
|
||||
kiwix::urlDecode(url),
|
||||
content,
|
||||
title,
|
||||
contentLength,
|
||||
contentType,
|
||||
stubRedirectUrl);
|
||||
}
|
||||
|
||||
bool Reader::getContentByEncodedUrl(const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType,
|
||||
string& baseUrl) const
|
||||
{
|
||||
return get_content_by_decoded_url(*this,
|
||||
kiwix::urlDecode(url),
|
||||
content,
|
||||
title,
|
||||
contentLength,
|
||||
contentType,
|
||||
baseUrl);
|
||||
}
|
||||
|
||||
bool Reader::getContentByEncodedUrl(const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType) const
|
||||
{
|
||||
std::string stubRedirectUrl;
|
||||
return get_content_by_decoded_url(*this,
|
||||
kiwix::urlDecode(url),
|
||||
content,
|
||||
title,
|
||||
contentLength,
|
||||
contentType,
|
||||
stubRedirectUrl);
|
||||
}
|
||||
|
||||
bool Reader::getContentByDecodedUrl(const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType) const
|
||||
{
|
||||
std::string stubRedirectUrl;
|
||||
return get_content_by_decoded_url(*this,
|
||||
url,
|
||||
content,
|
||||
title,
|
||||
contentLength,
|
||||
contentType,
|
||||
stubRedirectUrl);
|
||||
}
|
||||
|
||||
bool Reader::getContentByDecodedUrl(const string& url,
|
||||
string& content,
|
||||
string& title,
|
||||
unsigned int& contentLength,
|
||||
string& contentType,
|
||||
string& baseUrl) const
|
||||
{
|
||||
return get_content_by_decoded_url(*this,
|
||||
url,
|
||||
content,
|
||||
title,
|
||||
contentLength,
|
||||
contentType,
|
||||
baseUrl);
|
||||
}
|
||||
|
||||
/* Check if an article exists */
|
||||
bool Reader::urlExists(const string& url) const
|
||||
{
|
||||
return pathExists(url);
|
||||
}
|
||||
|
||||
bool Reader::pathExists(const string& path) const
|
||||
{
|
||||
if (!zimFileHandler)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
char ns = 0;
|
||||
string titleStr;
|
||||
_parseUrl(path, &ns, titleStr);
|
||||
zim::File::const_iterator findItr = zimFileHandler->find(ns, titleStr);
|
||||
return findItr != zimFileHandler->end() && findItr->getUrl() == titleStr;
|
||||
return zimArchive->hasEntryByPath(path);
|
||||
}
|
||||
|
||||
/* Does the ZIM file has a fulltext index */
|
||||
bool Reader::hasFulltextIndex() const
|
||||
{
|
||||
if (!zimFileHandler || zimFileHandler->is_multiPart() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ( pathExists("Z//fulltextIndex/xapian")
|
||||
|| pathExists("X/fulltext/xapian"));
|
||||
return zimArchive->hasFulltextIndex();
|
||||
}
|
||||
|
||||
/* Search titles by prefix */
|
||||
|
||||
bool Reader::searchSuggestions(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
const bool reset)
|
||||
{
|
||||
bool retVal = false;
|
||||
zim::File::const_iterator articleItr;
|
||||
|
||||
/* Reset the suggestions otherwise check if the suggestions number is less
|
||||
* than the suggestionsCount */
|
||||
if (reset) {
|
||||
@@ -791,39 +395,48 @@ bool Reader::searchSuggestions(const string& prefix,
|
||||
}
|
||||
}
|
||||
|
||||
auto ret = searchSuggestions(prefix, suggestionsCount, this->suggestions);
|
||||
|
||||
/* Set the cursor to the begining */
|
||||
this->suggestionsOffset = this->suggestions.begin();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
bool Reader::searchSuggestions(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
SuggestionsList_t& results)
|
||||
{
|
||||
bool retVal = false;
|
||||
|
||||
/* Return if no prefix */
|
||||
if (prefix.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (articleItr = zimFileHandler->findByTitle('A', prefix);
|
||||
articleItr != zimFileHandler->end()
|
||||
&& articleItr->getTitle().compare(0, prefix.size(), prefix) == 0
|
||||
&& this->suggestions.size() < suggestionsCount;
|
||||
++articleItr) {
|
||||
for (auto& entry: zimArchive->findByTitle(prefix)) {
|
||||
if (results.size() >= suggestionsCount) {
|
||||
break;
|
||||
}
|
||||
/* Extract the interesting part of article title & url */
|
||||
std::string normalizedArticleTitle
|
||||
= kiwix::normalize(articleItr->getTitle());
|
||||
std::string articleFinalUrl = "/A/" + articleItr->getUrl();
|
||||
if (articleItr->isRedirect()) {
|
||||
zim::Article article = *articleItr;
|
||||
unsigned int loopCounter = 0;
|
||||
while (article.isRedirect() && loopCounter++ < 42) {
|
||||
article = article.getRedirectArticle();
|
||||
}
|
||||
articleFinalUrl = "/A/" + article.getUrl();
|
||||
}
|
||||
= kiwix::normalize(entry.getTitle());
|
||||
|
||||
// Get the final path.
|
||||
auto item = entry.getItem(true);
|
||||
std::string articleFinalUrl = item.getPath();
|
||||
|
||||
/* Go through all already found suggestions and skip if this
|
||||
article is already in the suggestions list (with an other
|
||||
title) */
|
||||
bool insert = true;
|
||||
std::vector<std::vector<std::string>>::iterator suggestionItr;
|
||||
for (suggestionItr = this->suggestions.begin();
|
||||
suggestionItr != this->suggestions.end();
|
||||
std::vector<SuggestionItem>::iterator suggestionItr;
|
||||
for (suggestionItr = results.begin();
|
||||
suggestionItr != results.end();
|
||||
suggestionItr++) {
|
||||
int result = normalizedArticleTitle.compare((*suggestionItr)[2]);
|
||||
if (result == 0 && articleFinalUrl.compare((*suggestionItr)[1]) == 0) {
|
||||
int result = normalizedArticleTitle.compare((*suggestionItr).getNormalizedTitle());
|
||||
if (result == 0 && articleFinalUrl.compare((*suggestionItr).getPath()) == 0) {
|
||||
insert = false;
|
||||
break;
|
||||
} else if (result < 0) {
|
||||
@@ -833,20 +446,14 @@ bool Reader::searchSuggestions(const string& prefix,
|
||||
|
||||
/* Insert if possible */
|
||||
if (insert) {
|
||||
std::vector<std::string> suggestion;
|
||||
suggestion.push_back(articleItr->getTitle());
|
||||
suggestion.push_back(articleFinalUrl);
|
||||
suggestion.push_back(normalizedArticleTitle);
|
||||
this->suggestions.insert(suggestionItr, suggestion);
|
||||
SuggestionItem suggestion(entry.getTitle(), normalizedArticleTitle, articleFinalUrl);
|
||||
results.insert(suggestionItr, suggestion);
|
||||
}
|
||||
|
||||
/* Suggestions where found */
|
||||
retVal = true;
|
||||
}
|
||||
|
||||
/* Set the cursor to the begining */
|
||||
this->suggestionsOffset = this->suggestions.begin();
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@@ -861,39 +468,50 @@ std::vector<std::string> Reader::getTitleVariants(
|
||||
return variants;
|
||||
}
|
||||
|
||||
/* Try also a few variations of the prefix to have better results */
|
||||
|
||||
bool Reader::searchSuggestionsSmart(const string& prefix,
|
||||
unsigned int suggestionsCount)
|
||||
{
|
||||
this->suggestions.clear();
|
||||
this->suggestionsOffset = this->suggestions.begin();
|
||||
|
||||
auto ret = searchSuggestionsSmart(prefix, suggestionsCount, this->suggestions);
|
||||
|
||||
this->suggestionsOffset = this->suggestions.begin();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Try also a few variations of the prefix to have better results */
|
||||
bool Reader::searchSuggestionsSmart(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
SuggestionsList_t& results)
|
||||
{
|
||||
std::vector<std::string> variants = this->getTitleVariants(prefix);
|
||||
bool retVal = false;
|
||||
|
||||
this->suggestions.clear();
|
||||
this->suggestionsOffset = this->suggestions.begin();
|
||||
/* Try to search in the title using fulltext search database */
|
||||
const auto suggestionSearch
|
||||
= this->getZimFileHandler()->suggestions(prefix, 0, suggestionsCount);
|
||||
|
||||
if (suggestionSearch->get_matches_estimated()) {
|
||||
for (auto current = suggestionSearch->begin();
|
||||
current != suggestionSearch->end();
|
||||
auto suggestionSearcher = zim::Searcher(*zimArchive);
|
||||
zim::Query suggestionQuery;
|
||||
suggestionQuery.setQuery(prefix, true);
|
||||
auto suggestionSearch = suggestionSearcher.search(suggestionQuery);
|
||||
|
||||
if (suggestionSearch.getEstimatedMatches()) {
|
||||
const auto suggestions = suggestionSearch.getResults(0, suggestionsCount);
|
||||
for (auto current = suggestions.begin();
|
||||
current != suggestions.end();
|
||||
current++) {
|
||||
if (!current->good()) {
|
||||
continue;
|
||||
}
|
||||
std::vector<std::string> suggestion;
|
||||
suggestion.push_back(current->getTitle());
|
||||
suggestion.push_back("/A/" + current->getUrl());
|
||||
suggestion.push_back(kiwix::normalize(current->getTitle()));
|
||||
this->suggestions.push_back(suggestion);
|
||||
SuggestionItem suggestion(current.getTitle(), kiwix::normalize(current.getTitle()),
|
||||
current.getPath(), current.getSnippet());
|
||||
results.push_back(suggestion);
|
||||
}
|
||||
this->suggestionsOffset = this->suggestions.begin();
|
||||
retVal = true;
|
||||
} else {
|
||||
for (std::vector<std::string>::iterator variantsItr = variants.begin();
|
||||
variantsItr != variants.end();
|
||||
variantsItr++) {
|
||||
retVal = this->searchSuggestions(*variantsItr, suggestionsCount, false)
|
||||
retVal = this->searchSuggestions(*variantsItr, suggestionsCount, results)
|
||||
|| retVal;
|
||||
}
|
||||
}
|
||||
@@ -906,7 +524,7 @@ bool Reader::getNextSuggestion(string& title)
|
||||
{
|
||||
if (this->suggestionsOffset != this->suggestions.end()) {
|
||||
/* title */
|
||||
title = (*(this->suggestionsOffset))[0];
|
||||
title = (*(this->suggestionsOffset)).getTitle();
|
||||
|
||||
/* increment the cursor for the next call */
|
||||
this->suggestionsOffset++;
|
||||
@@ -921,8 +539,8 @@ bool Reader::getNextSuggestion(string& title, string& url)
|
||||
{
|
||||
if (this->suggestionsOffset != this->suggestions.end()) {
|
||||
/* title */
|
||||
title = (*(this->suggestionsOffset))[0];
|
||||
url = (*(this->suggestionsOffset))[1];
|
||||
title = (*(this->suggestionsOffset)).getTitle();
|
||||
url = (*(this->suggestionsOffset)).getPath();
|
||||
|
||||
/* increment the cursor for the next call */
|
||||
this->suggestionsOffset++;
|
||||
@@ -936,14 +554,14 @@ bool Reader::getNextSuggestion(string& title, string& url)
|
||||
/* Check if the file has as checksum */
|
||||
bool Reader::canCheckIntegrity() const
|
||||
{
|
||||
return this->zimFileHandler->getChecksum() != "";
|
||||
return zimArchive->hasChecksum();
|
||||
}
|
||||
|
||||
/* Return true if corrupted, false otherwise */
|
||||
bool Reader::isCorrupted() const
|
||||
{
|
||||
try {
|
||||
if (this->zimFileHandler->verify() == true) {
|
||||
if (zimArchive->check() == true) {
|
||||
return false;
|
||||
}
|
||||
} catch (exception& e) {
|
||||
@@ -957,13 +575,7 @@ bool Reader::isCorrupted() const
|
||||
/* Return the file size, works also for splitted files */
|
||||
unsigned int Reader::getFileSize() const
|
||||
{
|
||||
zim::File* file = this->getZimFileHandler();
|
||||
zim::size_type size = 0;
|
||||
|
||||
if (file != NULL) {
|
||||
size = file->getFilesize();
|
||||
}
|
||||
|
||||
return (size / 1024);
|
||||
return zimArchive->getFilesize() / 1024;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -77,9 +77,7 @@ std::string SearchRenderer::getHtml()
|
||||
result.set("title", p_result->get_title());
|
||||
result.set("url", p_result->get_url());
|
||||
result.set("snippet", p_result->get_snippet());
|
||||
auto readerIndex = p_result->get_readerIndex();
|
||||
auto reader = mp_searcher->get_reader(readerIndex);
|
||||
result.set("resultContentId", mp_nameMapper->getNameForId(reader->getId()));
|
||||
result.set("resultContentId", mp_nameMapper->getNameForId(p_result->get_zimId()));
|
||||
|
||||
if (p_result->get_wordCount() >= 0) {
|
||||
result.set("wordCount", kiwix::beautifyInteger(p_result->get_wordCount()));
|
||||
@@ -93,30 +91,30 @@ std::string SearchRenderer::getHtml()
|
||||
kainjow::mustache::data pages{kainjow::mustache::data::type::list};
|
||||
|
||||
auto resultStart = mp_searcher->getResultStart();
|
||||
auto resultEnd = mp_searcher->getResultEnd();
|
||||
auto resultCountPerPage = resultEnd - resultStart;
|
||||
auto resultEnd = 0U;
|
||||
auto estimatedResultCount = mp_searcher->getEstimatedResultCount();
|
||||
auto currentPage = 0U;
|
||||
auto pageStart = 0U;
|
||||
auto pageEnd = 0U;
|
||||
auto lastPageStart = 0U;
|
||||
if (resultCountPerPage) {
|
||||
currentPage = resultStart/resultCountPerPage;
|
||||
if (pageLength) {
|
||||
currentPage = resultStart/pageLength;
|
||||
pageStart = currentPage > 4 ? currentPage-4 : 0;
|
||||
pageEnd = currentPage + 5;
|
||||
if (pageEnd > estimatedResultCount / resultCountPerPage) {
|
||||
pageEnd = estimatedResultCount / resultCountPerPage;
|
||||
if (pageEnd > estimatedResultCount / pageLength) {
|
||||
pageEnd = (estimatedResultCount + pageLength - 1) / pageLength;
|
||||
}
|
||||
if (estimatedResultCount > resultCountPerPage) {
|
||||
lastPageStart = static_cast<int>(round(estimatedResultCount/resultCountPerPage)) * resultCountPerPage;
|
||||
if (estimatedResultCount > pageLength) {
|
||||
lastPageStart = ((estimatedResultCount-1)/pageLength)*pageLength;
|
||||
}
|
||||
}
|
||||
|
||||
resultEnd = resultStart+pageLength; //setting result end
|
||||
|
||||
for (unsigned int i = pageStart; i < pageEnd; i++) {
|
||||
kainjow::mustache::data page;
|
||||
page.set("label", to_string(i + 1));
|
||||
page.set("start", to_string(i * resultCountPerPage));
|
||||
page.set("end", to_string((i + 1) * resultCountPerPage));
|
||||
page.set("start", to_string(i * pageLength));
|
||||
|
||||
if (i == currentPage) {
|
||||
page.set("selected", true);
|
||||
@@ -137,9 +135,8 @@ std::string SearchRenderer::getHtml()
|
||||
allData.set("searchPatternEncoded", urlEncode(this->searchPattern));
|
||||
allData.set("resultStart", to_string(resultStart + 1));
|
||||
allData.set("resultEnd", to_string(min(resultEnd, estimatedResultCount)));
|
||||
allData.set("resultRange", to_string(resultCountPerPage));
|
||||
allData.set("pageLength", to_string(pageLength));
|
||||
allData.set("resultLastPageStart", to_string(lastPageStart));
|
||||
allData.set("lastResult", to_string(estimatedResultCount));
|
||||
allData.set("protocolPrefix", this->protocolPrefix);
|
||||
allData.set("searchProtocolPrefix", this->searchProtocolPrefix);
|
||||
allData.set("contentId", this->searchContent);
|
||||
@@ -149,4 +146,4 @@ std::string SearchRenderer::getHtml()
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
117
src/searcher.cpp
117
src/searcher.cpp
@@ -35,7 +35,7 @@ namespace kiwix
|
||||
class _Result : public Result
|
||||
{
|
||||
public:
|
||||
_Result(zim::Search::iterator& iterator);
|
||||
_Result(zim::SearchResultSet::iterator iterator);
|
||||
virtual ~_Result(){};
|
||||
|
||||
virtual std::string get_url();
|
||||
@@ -45,29 +45,25 @@ class _Result : public Result
|
||||
virtual std::string get_content();
|
||||
virtual int get_wordCount();
|
||||
virtual int get_size();
|
||||
virtual int get_readerIndex();
|
||||
virtual std::string get_zimId();
|
||||
|
||||
private:
|
||||
zim::Search::iterator iterator;
|
||||
zim::SearchResultSet::iterator iterator;
|
||||
};
|
||||
|
||||
struct SearcherInternal {
|
||||
const zim::Search* _search;
|
||||
zim::Search::iterator current_iterator;
|
||||
|
||||
SearcherInternal() : _search(NULL) {}
|
||||
~SearcherInternal()
|
||||
struct SearcherInternal : zim::SearchResultSet {
|
||||
explicit SearcherInternal(const zim::SearchResultSet& srs)
|
||||
: zim::SearchResultSet(srs)
|
||||
, current_iterator(srs.begin())
|
||||
{
|
||||
if (_search != NULL) {
|
||||
delete _search;
|
||||
}
|
||||
}
|
||||
|
||||
zim::SearchResultSet::iterator current_iterator;
|
||||
};
|
||||
|
||||
/* Constructor */
|
||||
Searcher::Searcher()
|
||||
: internal(new SearcherInternal()),
|
||||
searchPattern(""),
|
||||
: searchPattern(""),
|
||||
estimatedResultCount(0),
|
||||
resultStart(0),
|
||||
resultEnd(0)
|
||||
@@ -78,7 +74,6 @@ Searcher::Searcher()
|
||||
/* Destructor */
|
||||
Searcher::~Searcher()
|
||||
{
|
||||
delete internal;
|
||||
}
|
||||
|
||||
bool Searcher::add_reader(Reader* reader)
|
||||
@@ -97,7 +92,7 @@ Reader* Searcher::get_reader(int readerIndex)
|
||||
}
|
||||
|
||||
/* Search strings in the database */
|
||||
void Searcher::search(std::string& search,
|
||||
void Searcher::search(const std::string& search,
|
||||
unsigned int resultStart,
|
||||
unsigned int resultEnd,
|
||||
const bool verbose)
|
||||
@@ -115,20 +110,20 @@ void Searcher::search(std::string& search,
|
||||
if (resultStart != resultEnd) {
|
||||
/* Perform the search */
|
||||
string unaccentedSearch = removeAccents(search);
|
||||
std::vector<const zim::File*> zims;
|
||||
std::vector<zim::Archive> archives;
|
||||
for (auto current = this->readers.begin(); current != this->readers.end();
|
||||
current++) {
|
||||
if ( (*current)->hasFulltextIndex() ) {
|
||||
zims.push_back((*current)->getZimFileHandler());
|
||||
archives.push_back(*(*current)->getZimArchive());
|
||||
}
|
||||
}
|
||||
zim::Search* search = new zim::Search(zims);
|
||||
search->set_verbose(verbose);
|
||||
search->set_query(unaccentedSearch);
|
||||
search->set_range(resultStart, resultEnd);
|
||||
internal->_search = search;
|
||||
internal->current_iterator = internal->_search->begin();
|
||||
this->estimatedResultCount = internal->_search->get_matches_estimated();
|
||||
zim::Searcher searcher(archives);
|
||||
zim::Query query;
|
||||
query.setQuery(unaccentedSearch, false);
|
||||
query.setVerbose(verbose);
|
||||
zim::Search search = searcher.search(query);
|
||||
internal.reset(new SearcherInternal(search.getResults(resultStart, resultEnd)));
|
||||
this->estimatedResultCount = search.getEstimatedMatches();
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -158,33 +153,33 @@ void Searcher::geo_search(float latitude, float longitude, float distance,
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<const zim::File*> zims;
|
||||
std::vector<zim::Archive> archives;
|
||||
for (auto current = this->readers.begin(); current != this->readers.end();
|
||||
current++) {
|
||||
zims.push_back((*current)->getZimFileHandler());
|
||||
archives.push_back(*(*current)->getZimArchive());
|
||||
}
|
||||
zim::Search* search = new zim::Search(zims);
|
||||
search->set_verbose(verbose);
|
||||
search->set_query("");
|
||||
search->set_georange(latitude, longitude, distance);
|
||||
search->set_range(resultStart, resultEnd);
|
||||
internal->_search = search;
|
||||
internal->current_iterator = internal->_search->begin();
|
||||
this->estimatedResultCount = internal->_search->get_matches_estimated();
|
||||
zim::Searcher searcher(archives);
|
||||
zim::Query query;
|
||||
query.setVerbose(verbose);
|
||||
query.setQuery("", false);
|
||||
query.setGeorange(latitude, longitude, distance);
|
||||
zim::Search search = searcher.search(query);
|
||||
internal.reset(new SearcherInternal(search.getResults(resultStart, resultEnd)));
|
||||
this->estimatedResultCount = search.getEstimatedMatches();
|
||||
}
|
||||
|
||||
|
||||
void Searcher::restart_search()
|
||||
{
|
||||
if (internal->_search) {
|
||||
internal->current_iterator = internal->_search->begin();
|
||||
if (internal.get()) {
|
||||
internal->current_iterator = internal->begin();
|
||||
}
|
||||
}
|
||||
|
||||
Result* Searcher::getNextResult()
|
||||
{
|
||||
if (internal->_search &&
|
||||
internal->current_iterator != internal->_search->end()) {
|
||||
if (internal.get() &&
|
||||
internal->current_iterator != internal->end()) {
|
||||
Result* result = new _Result(internal->current_iterator);
|
||||
internal->current_iterator++;
|
||||
return result;
|
||||
@@ -213,19 +208,18 @@ void Searcher::suggestions(std::string& searchPattern, const bool verbose)
|
||||
this->resultEnd = 10;
|
||||
string unaccentedSearch = removeAccents(searchPattern);
|
||||
|
||||
std::vector<const zim::File*> zims;
|
||||
std::vector<zim::Archive> archives;
|
||||
for (auto current = this->readers.begin(); current != this->readers.end();
|
||||
current++) {
|
||||
zims.push_back((*current)->getZimFileHandler());
|
||||
archives.push_back(*(*current)->getZimArchive());
|
||||
}
|
||||
zim::Search* search = new zim::Search(zims);
|
||||
search->set_verbose(verbose);
|
||||
search->set_query(unaccentedSearch);
|
||||
search->set_range(resultStart, resultEnd);
|
||||
search->set_suggestion_mode(true);
|
||||
internal->_search = search;
|
||||
internal->current_iterator = internal->_search->begin();
|
||||
this->estimatedResultCount = internal->_search->get_matches_estimated();
|
||||
zim::Searcher searcher(archives);
|
||||
zim::Query query;
|
||||
query.setVerbose(verbose);
|
||||
query.setQuery(unaccentedSearch, true);
|
||||
zim::Search search = searcher.search(query);
|
||||
internal.reset(new SearcherInternal(search.getResults(resultStart, resultEnd)));
|
||||
this->estimatedResultCount = search.getEstimatedMatches();
|
||||
}
|
||||
|
||||
/* Return the result count estimation */
|
||||
@@ -234,45 +228,44 @@ unsigned int Searcher::getEstimatedResultCount()
|
||||
return this->estimatedResultCount;
|
||||
}
|
||||
|
||||
_Result::_Result(zim::Search::iterator& iterator)
|
||||
_Result::_Result(zim::SearchResultSet::iterator iterator)
|
||||
: iterator(iterator)
|
||||
{
|
||||
}
|
||||
|
||||
std::string _Result::get_url()
|
||||
{
|
||||
return iterator.get_url();
|
||||
return iterator.getPath();
|
||||
}
|
||||
std::string _Result::get_title()
|
||||
{
|
||||
return iterator.get_title();
|
||||
return iterator.getTitle();
|
||||
}
|
||||
int _Result::get_score()
|
||||
{
|
||||
return iterator.get_score();
|
||||
return iterator.getScore();
|
||||
}
|
||||
std::string _Result::get_snippet()
|
||||
{
|
||||
return iterator.get_snippet();
|
||||
return iterator.getSnippet();
|
||||
}
|
||||
std::string _Result::get_content()
|
||||
{
|
||||
if (iterator->good()) {
|
||||
return iterator->getData();
|
||||
}
|
||||
return "";
|
||||
return iterator->getItem(true).getData();
|
||||
}
|
||||
int _Result::get_size()
|
||||
{
|
||||
return iterator.get_size();
|
||||
return iterator.getSize();
|
||||
}
|
||||
int _Result::get_wordCount()
|
||||
{
|
||||
return iterator.get_wordCount();
|
||||
return iterator.getWordCount();
|
||||
}
|
||||
int _Result::get_readerIndex()
|
||||
std::string _Result::get_zimId()
|
||||
{
|
||||
return iterator.get_fileIndex();
|
||||
std::ostringstream s;
|
||||
s << iterator.getZimId();
|
||||
return s.str();
|
||||
}
|
||||
|
||||
|
||||
|
||||
840
src/server.cpp
840
src/server.cpp
@@ -19,125 +19,15 @@
|
||||
|
||||
#include "server.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
# if !defined(__MINGW32__) && (_MSC_VER < 1600)
|
||||
# include "stdint4win.h"
|
||||
# endif
|
||||
# include <winsock2.h>
|
||||
# include <ws2tcpip.h>
|
||||
# ifdef __GNUC__
|
||||
// inet_pton is not declared in mingw, even if the function exists.
|
||||
extern "C" {
|
||||
WINSOCK_API_LINKAGE INT WSAAPI inet_pton( INT Family, PCSTR pszAddrString, PVOID pAddrBuf);
|
||||
}
|
||||
# endif
|
||||
typedef UINT64 uint64_t;
|
||||
typedef UINT16 uint16_t;
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
#include <microhttpd.h>
|
||||
}
|
||||
|
||||
#include "tools/otherTools.h"
|
||||
#include "tools/pathTools.h"
|
||||
#include "tools/regexTools.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include "library.h"
|
||||
#include "name_mapper.h"
|
||||
#include "entry.h"
|
||||
#include "searcher.h"
|
||||
#include "search_renderer.h"
|
||||
#include "opds_dumper.h"
|
||||
|
||||
#include <zim/uuid.h>
|
||||
|
||||
#include <mustache.hpp>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include "kiwixlib-resources.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
# include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
#include "server/request_context.h"
|
||||
#include "server/response.h"
|
||||
|
||||
#define MAX_SEARCH_LEN 140
|
||||
#define KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE 100
|
||||
#include "server/internalServer.h"
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
static IdNameMapper defaultNameMapper;
|
||||
|
||||
static int staticHandlerCallback(void* cls,
|
||||
struct MHD_Connection* connection,
|
||||
const char* url,
|
||||
const char* method,
|
||||
const char* version,
|
||||
const char* upload_data,
|
||||
size_t* upload_data_size,
|
||||
void** cont_cls);
|
||||
|
||||
|
||||
class InternalServer {
|
||||
public:
|
||||
InternalServer(Library* library,
|
||||
NameMapper* nameMapper,
|
||||
std::string addr,
|
||||
int port,
|
||||
std::string root,
|
||||
int nbThreads,
|
||||
bool verbose,
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton);
|
||||
virtual ~InternalServer() = default;
|
||||
|
||||
int handlerCallback(struct MHD_Connection* connection,
|
||||
const char* url,
|
||||
const char* method,
|
||||
const char* version,
|
||||
const char* upload_data,
|
||||
size_t* upload_data_size,
|
||||
void** cont_cls);
|
||||
bool start();
|
||||
void stop();
|
||||
|
||||
private:
|
||||
Response handle_request(const RequestContext& request);
|
||||
Response build_500(const std::string& msg);
|
||||
Response build_404(const RequestContext& request, const std::string& zimName);
|
||||
Response build_homepage(const RequestContext& request);
|
||||
Response handle_skin(const RequestContext& request);
|
||||
Response handle_catalog(const RequestContext& request);
|
||||
Response handle_meta(const RequestContext& request);
|
||||
Response handle_search(const RequestContext& request);
|
||||
Response handle_suggest(const RequestContext& request);
|
||||
Response handle_random(const RequestContext& request);
|
||||
Response handle_content(const RequestContext& request);
|
||||
|
||||
kainjow::mustache::data get_default_data();
|
||||
Response get_default_response();
|
||||
|
||||
std::string m_addr;
|
||||
int m_port;
|
||||
std::string m_root;
|
||||
int m_nbThreads;
|
||||
std::atomic_bool m_verbose;
|
||||
bool m_withTaskbar;
|
||||
bool m_withLibraryButton;
|
||||
struct MHD_Daemon* mp_daemon;
|
||||
|
||||
Library* mp_library;
|
||||
NameMapper* mp_nameMapper;
|
||||
};
|
||||
|
||||
|
||||
Server::Server(Library* library, NameMapper* nameMapper) :
|
||||
mp_library(library),
|
||||
mp_nameMapper(nameMapper),
|
||||
@@ -157,13 +47,16 @@ bool Server::start() {
|
||||
m_nbThreads,
|
||||
m_verbose,
|
||||
m_withTaskbar,
|
||||
m_withLibraryButton));
|
||||
m_withLibraryButton,
|
||||
m_blockExternalLinks));
|
||||
return mp_server->start();
|
||||
}
|
||||
|
||||
void Server::stop() {
|
||||
mp_server->stop();
|
||||
mp_server.reset(nullptr);
|
||||
if (mp_server) {
|
||||
mp_server->stop();
|
||||
mp_server.reset(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::setRoot(const std::string& root)
|
||||
@@ -177,723 +70,4 @@ void Server::setRoot(const std::string& root)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
InternalServer::InternalServer(Library* library,
|
||||
NameMapper* nameMapper,
|
||||
std::string addr,
|
||||
int port,
|
||||
std::string root,
|
||||
int nbThreads,
|
||||
bool verbose,
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton) :
|
||||
m_addr(addr),
|
||||
m_port(port),
|
||||
m_root(root),
|
||||
m_nbThreads(nbThreads),
|
||||
m_verbose(verbose),
|
||||
m_withTaskbar(withTaskbar),
|
||||
m_withLibraryButton(withLibraryButton),
|
||||
mp_daemon(nullptr),
|
||||
mp_library(library),
|
||||
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper)
|
||||
{}
|
||||
|
||||
bool InternalServer::start() {
|
||||
#ifdef _WIN32
|
||||
int flags = MHD_USE_SELECT_INTERNALLY;
|
||||
#else
|
||||
int flags = MHD_USE_POLL_INTERNALLY;
|
||||
#endif
|
||||
if (m_verbose.load())
|
||||
flags |= MHD_USE_DEBUG;
|
||||
|
||||
|
||||
struct sockaddr_in sockAddr;
|
||||
memset(&sockAddr, 0, sizeof(sockAddr));
|
||||
sockAddr.sin_family = AF_INET;
|
||||
sockAddr.sin_port = htons(m_port);
|
||||
if (m_addr.empty()) {
|
||||
if (0 != INADDR_ANY)
|
||||
sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
} else {
|
||||
if (inet_pton(AF_INET, m_addr.c_str(), &(sockAddr.sin_addr.s_addr)) == 0) {
|
||||
std::cerr << "Ip address " << m_addr << " is not a valid ip address" << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
mp_daemon = MHD_start_daemon(flags,
|
||||
m_port,
|
||||
NULL,
|
||||
NULL,
|
||||
&staticHandlerCallback,
|
||||
this,
|
||||
MHD_OPTION_SOCK_ADDR, &sockAddr,
|
||||
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
|
||||
MHD_OPTION_END);
|
||||
if (mp_daemon == nullptr) {
|
||||
std::cerr << "Unable to instantiate the HTTP daemon. The port " << m_port
|
||||
<< " is maybe already occupied or need more permissions to be open. "
|
||||
"Please try as root or with a port number higher or equal to 1024."
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void InternalServer::stop()
|
||||
{
|
||||
MHD_stop_daemon(mp_daemon);
|
||||
}
|
||||
|
||||
static int staticHandlerCallback(void* cls,
|
||||
struct MHD_Connection* connection,
|
||||
const char* url,
|
||||
const char* method,
|
||||
const char* version,
|
||||
const char* upload_data,
|
||||
size_t* upload_data_size,
|
||||
void** cont_cls)
|
||||
{
|
||||
InternalServer* _this = static_cast<InternalServer*>(cls);
|
||||
|
||||
return _this->handlerCallback(connection,
|
||||
url,
|
||||
method,
|
||||
version,
|
||||
upload_data,
|
||||
upload_data_size,
|
||||
cont_cls);
|
||||
}
|
||||
|
||||
int InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
const char* url,
|
||||
const char* method,
|
||||
const char* version,
|
||||
const char* upload_data,
|
||||
size_t* upload_data_size,
|
||||
void** cont_cls)
|
||||
{
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
if (m_verbose.load() ) {
|
||||
printf("======================\n");
|
||||
printf("Requesting : \n");
|
||||
printf("full_url : %s\n", url);
|
||||
}
|
||||
RequestContext request(connection, m_root, url, method, version);
|
||||
|
||||
if (m_verbose.load() ) {
|
||||
request.print_debug_info();
|
||||
}
|
||||
/* Unexpected method */
|
||||
if (request.get_method() != RequestMethod::GET
|
||||
&& request.get_method() != RequestMethod::POST) {
|
||||
printf("Reject request because of unhandled request method.\n");
|
||||
printf("----------------------\n");
|
||||
return MHD_NO;
|
||||
}
|
||||
|
||||
auto response = handle_request(request);
|
||||
|
||||
if (response.getReturnCode() == MHD_HTTP_INTERNAL_SERVER_ERROR) {
|
||||
printf("========== INTERNAL ERROR !! ============\n");
|
||||
if (!m_verbose.load()) {
|
||||
printf("Requesting : \n");
|
||||
printf("full_url : %s\n", url);
|
||||
request.print_debug_info();
|
||||
}
|
||||
}
|
||||
|
||||
auto ret = response.send(request, connection);
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(end_time - start_time);
|
||||
if (m_verbose.load()) {
|
||||
printf("Request time : %fs\n", time_span.count());
|
||||
printf("----------------------\n");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
Response InternalServer::handle_request(const RequestContext& request)
|
||||
{
|
||||
try {
|
||||
if (! request.is_valid_url())
|
||||
return build_404(request, "");
|
||||
|
||||
if (kiwix::startsWith(request.get_url(), "/skin/"))
|
||||
return handle_skin(request);
|
||||
|
||||
if (startsWith(request.get_url(), "/catalog"))
|
||||
return handle_catalog(request);
|
||||
|
||||
if (request.get_url() == "/meta")
|
||||
return handle_meta(request);
|
||||
|
||||
if (request.get_url() == "/search")
|
||||
return handle_search(request);
|
||||
|
||||
if (request.get_url() == "/suggest")
|
||||
return handle_suggest(request);
|
||||
|
||||
if (request.get_url() == "/random")
|
||||
return handle_random(request);
|
||||
|
||||
return handle_content(request);
|
||||
} catch (std::exception& e) {
|
||||
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
|
||||
return build_500(e.what());
|
||||
} catch (...) {
|
||||
fprintf(stderr, "===== Unhandled unknown error\n");
|
||||
return build_500("Unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
kainjow::mustache::data InternalServer::get_default_data()
|
||||
{
|
||||
kainjow::mustache::data data;
|
||||
data.set("root", m_root);
|
||||
return data;
|
||||
}
|
||||
|
||||
Response InternalServer::get_default_response()
|
||||
{
|
||||
return Response(m_root, m_verbose.load(), m_withTaskbar, m_withLibraryButton);
|
||||
}
|
||||
|
||||
|
||||
Response InternalServer::build_404(const RequestContext& request,
|
||||
const std::string& bookName)
|
||||
{
|
||||
kainjow::mustache::data results;
|
||||
results.set("url", request.get_full_url());
|
||||
|
||||
auto response = get_default_response();
|
||||
response.set_template(RESOURCE::templates::_404_html, results);
|
||||
response.set_mimeType("text/html");
|
||||
response.set_code(MHD_HTTP_NOT_FOUND);
|
||||
response.set_compress(true);
|
||||
response.set_taskbar(bookName, "");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response InternalServer::build_500(const std::string& msg)
|
||||
{
|
||||
kainjow::mustache::data data;
|
||||
data.set("error", msg);
|
||||
Response response(m_root, true, false, false);
|
||||
response.set_template(RESOURCE::templates::_500_html, data);
|
||||
response.set_mimeType("text/html");
|
||||
response.set_code(MHD_HTTP_INTERNAL_SERVER_ERROR);
|
||||
return response;
|
||||
}
|
||||
|
||||
Response InternalServer::build_homepage(const RequestContext& request)
|
||||
{
|
||||
auto data = get_default_data();
|
||||
|
||||
kainjow::mustache::data books{kainjow::mustache::data::type::list};
|
||||
for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
|
||||
auto& currentBook = mp_library->getBookById(bookId);
|
||||
|
||||
kainjow::mustache::data book;
|
||||
book.set("name", mp_nameMapper->getNameForId(bookId));
|
||||
book.set("title", currentBook.getTitle());
|
||||
book.set("description", currentBook.getDescription());
|
||||
book.set("articleCount", beautifyInteger(currentBook.getArticleCount()));
|
||||
book.set("mediaCount", beautifyInteger(currentBook.getMediaCount()));
|
||||
books.push_back(book);
|
||||
}
|
||||
|
||||
data.set("books", books);
|
||||
|
||||
auto response = get_default_response();
|
||||
response.set_template(RESOURCE::templates::index_html, data);
|
||||
response.set_mimeType("text/html; charset=utf-8");
|
||||
response.set_compress(true);
|
||||
response.set_taskbar("", "");
|
||||
return response;
|
||||
}
|
||||
|
||||
Response InternalServer::handle_meta(const RequestContext& request)
|
||||
{
|
||||
std::string bookName;
|
||||
std::string bookId;
|
||||
std::string meta_name;
|
||||
std::shared_ptr<Reader> reader;
|
||||
try {
|
||||
bookName = request.get_argument("content");
|
||||
bookId = mp_nameMapper->getIdForName(bookName);
|
||||
meta_name = request.get_argument("name");
|
||||
reader = mp_library->getReaderById(bookId);
|
||||
} catch (const std::out_of_range& e) {
|
||||
return build_404(request, bookName);
|
||||
}
|
||||
|
||||
if (reader == nullptr) {
|
||||
return build_404(request, bookName);
|
||||
}
|
||||
|
||||
std::string content;
|
||||
std::string mimeType = "text";
|
||||
|
||||
if (meta_name == "title") {
|
||||
content = reader->getTitle();
|
||||
} else if (meta_name == "description") {
|
||||
content = reader->getDescription();
|
||||
} else if (meta_name == "language") {
|
||||
content = reader->getLanguage();
|
||||
} else if (meta_name == "name") {
|
||||
content = reader->getName();
|
||||
} else if (meta_name == "tags") {
|
||||
content = reader->getTags();
|
||||
} else if (meta_name == "date") {
|
||||
content = reader->getDate();
|
||||
} else if (meta_name == "creator") {
|
||||
content = reader->getCreator();
|
||||
} else if (meta_name == "publisher") {
|
||||
content = reader->getPublisher();
|
||||
} else if (meta_name == "favicon") {
|
||||
reader->getFavicon(content, mimeType);
|
||||
} else {
|
||||
return build_404(request, bookName);
|
||||
}
|
||||
|
||||
auto response = get_default_response();
|
||||
response.set_content(content);
|
||||
response.set_mimeType(mimeType);
|
||||
response.set_compress(false);
|
||||
response.set_cache(true);
|
||||
return response;
|
||||
}
|
||||
|
||||
Response InternalServer::handle_suggest(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_suggest\n");
|
||||
}
|
||||
|
||||
std::string content;
|
||||
std::string mimeType;
|
||||
unsigned int maxSuggestionCount = 10;
|
||||
unsigned int suggestionCount = 0;
|
||||
std::string suggestion;
|
||||
|
||||
std::string bookName;
|
||||
std::string bookId;
|
||||
std::string term;
|
||||
std::shared_ptr<Reader> reader;
|
||||
try {
|
||||
bookName = request.get_argument("content");
|
||||
bookId = mp_nameMapper->getIdForName(bookName);
|
||||
term = request.get_argument("term");
|
||||
reader = mp_library->getReaderById(bookId);
|
||||
} catch (const std::out_of_range&) {
|
||||
return build_404(request, bookName);
|
||||
}
|
||||
|
||||
if (m_verbose.load()) {
|
||||
printf("Searching suggestions for: \"%s\"\n", term.c_str());
|
||||
}
|
||||
|
||||
kainjow::mustache::data results{kainjow::mustache::data::type::list};
|
||||
|
||||
bool first = true;
|
||||
if (reader != nullptr) {
|
||||
/* Get the suggestions */
|
||||
reader->searchSuggestionsSmart(term, maxSuggestionCount);
|
||||
while (reader->getNextSuggestion(suggestion)) {
|
||||
kainjow::mustache::data result;
|
||||
result.set("label", suggestion);
|
||||
result.set("value", suggestion);
|
||||
result.set("first", first);
|
||||
first = false;
|
||||
results.push_back(result);
|
||||
suggestionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Propose the fulltext search if possible */
|
||||
if (reader->hasFulltextIndex()) {
|
||||
kainjow::mustache::data result;
|
||||
result.set("label", "containing '" + term + "'...");
|
||||
result.set("value", term + " ");
|
||||
result.set("first", first);
|
||||
results.push_back(result);
|
||||
}
|
||||
|
||||
auto data = get_default_data();
|
||||
data.set("suggestions", results);
|
||||
|
||||
auto response = get_default_response();
|
||||
response.set_template(RESOURCE::templates::suggestion_json, data);
|
||||
response.set_mimeType("application/json; charset=utf-8");
|
||||
response.set_compress(true);
|
||||
return response;
|
||||
}
|
||||
|
||||
Response InternalServer::handle_skin(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_skin\n");
|
||||
}
|
||||
|
||||
auto response = get_default_response();
|
||||
auto resourceName = request.get_url().substr(1);
|
||||
try {
|
||||
response.set_content(getResource(resourceName));
|
||||
} catch (const ResourceNotFound& e) {
|
||||
return build_404(request, "");
|
||||
}
|
||||
response.set_mimeType(getMimeTypeForFile(resourceName));
|
||||
response.set_compress(true);
|
||||
response.set_cache(true);
|
||||
return response;
|
||||
}
|
||||
|
||||
Response InternalServer::handle_search(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_search\n");
|
||||
}
|
||||
|
||||
std::string bookName;
|
||||
std::string bookId;
|
||||
try {
|
||||
bookName = request.get_argument("content");
|
||||
bookId = mp_nameMapper->getIdForName(bookName);
|
||||
} catch (const std::out_of_range&) {}
|
||||
|
||||
std::string patternString;
|
||||
try {
|
||||
patternString = request.get_argument("pattern");
|
||||
} catch (const std::out_of_range&) {}
|
||||
|
||||
/* Retrive geo search */
|
||||
bool has_geo_query = false;
|
||||
float latitude = 0;
|
||||
float longitude = 0;
|
||||
float distance = 0;
|
||||
try {
|
||||
latitude = request.get_argument<float>("latitude");
|
||||
longitude = request.get_argument<float>("longitude");
|
||||
distance = request.get_argument<float>("distance");
|
||||
has_geo_query = true;
|
||||
} catch(const std::out_of_range&) {}
|
||||
catch(const std::invalid_argument&) {}
|
||||
|
||||
std::shared_ptr<Reader> reader(nullptr);
|
||||
try {
|
||||
reader = mp_library->getReaderById(bookId);
|
||||
} catch (const std::out_of_range&) {}
|
||||
|
||||
/* Try first to load directly the article */
|
||||
if (reader != nullptr && !patternString.empty()) {
|
||||
std::string patternCorrespondingUrl;
|
||||
auto variants = reader->getTitleVariants(patternString);
|
||||
auto variantsItr = variants.begin();
|
||||
|
||||
while (patternCorrespondingUrl.empty() && variantsItr != variants.end()) {
|
||||
try {
|
||||
auto entry = reader->getEntryFromTitle(*variantsItr);
|
||||
entry = entry.getFinalEntry();
|
||||
patternCorrespondingUrl = entry.getPath();
|
||||
break;
|
||||
} catch(kiwix::NoEntry& e) {
|
||||
variantsItr++;
|
||||
}
|
||||
}
|
||||
|
||||
/* If article found then redirect directly to it */
|
||||
if (!patternCorrespondingUrl.empty()) {
|
||||
auto response = get_default_response();
|
||||
response.set_redirection(m_root + "/" + bookName + "/" + patternCorrespondingUrl);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/* Make the search */
|
||||
auto response = get_default_response();
|
||||
response.set_mimeType("text/html; charset=utf-8");
|
||||
response.set_taskbar(bookName, reader ? reader->getTitle() : "");
|
||||
response.set_compress(true);
|
||||
|
||||
if ( (!reader && !bookName.empty())
|
||||
|| (patternString.empty() && ! has_geo_query) ) {
|
||||
auto data = get_default_data();
|
||||
data.set("pattern", encodeDiples(patternString));
|
||||
response.set_template(RESOURCE::templates::no_search_result_html, data);
|
||||
response.set_code(MHD_HTTP_NOT_FOUND);
|
||||
return response;
|
||||
}
|
||||
|
||||
Searcher searcher;
|
||||
if (reader) {
|
||||
searcher.add_reader(reader.get());
|
||||
} else {
|
||||
for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
|
||||
auto currentReader = mp_library->getReaderById(bookId);
|
||||
if (currentReader) {
|
||||
searcher.add_reader(currentReader.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto start = 0;
|
||||
try {
|
||||
start = request.get_argument<unsigned int>("start");
|
||||
} catch (const std::exception&) {}
|
||||
auto end = 25;
|
||||
try {
|
||||
end = request.get_argument<unsigned int>("end");
|
||||
} catch (const std::exception&) {}
|
||||
if (start>end) {
|
||||
auto tmp = start;
|
||||
start = end;
|
||||
end = tmp;
|
||||
}
|
||||
if (end > start + MAX_SEARCH_LEN) {
|
||||
end = start + MAX_SEARCH_LEN;
|
||||
}
|
||||
|
||||
/* Get the results */
|
||||
try {
|
||||
if (patternString.empty()) {
|
||||
searcher.geo_search(latitude, longitude, distance,
|
||||
start, end, m_verbose.load());
|
||||
} else {
|
||||
searcher.search(patternString,
|
||||
start, end, m_verbose.load());
|
||||
}
|
||||
SearchRenderer renderer(&searcher, mp_nameMapper);
|
||||
renderer.setSearchPattern(patternString);
|
||||
renderer.setSearchContent(bookName);
|
||||
renderer.setProtocolPrefix(m_root + "/");
|
||||
renderer.setSearchProtocolPrefix(m_root + "/search?");
|
||||
response.set_content(renderer.getHtml());
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << e.what() << std::endl;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Response InternalServer::handle_random(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_random\n");
|
||||
}
|
||||
|
||||
std::string bookName;
|
||||
std::string bookId;
|
||||
std::shared_ptr<Reader> reader;
|
||||
try {
|
||||
bookName = request.get_argument("content");
|
||||
bookId = mp_nameMapper->getIdForName(bookName);
|
||||
reader = mp_library->getReaderById(bookId);
|
||||
} catch (const std::out_of_range&) {
|
||||
return build_404(request, bookName);
|
||||
}
|
||||
|
||||
if (reader == nullptr) {
|
||||
return build_404(request, bookName);
|
||||
}
|
||||
|
||||
try {
|
||||
auto entry = reader->getRandomPage();
|
||||
entry = entry.getFinalEntry();
|
||||
auto response = get_default_response();
|
||||
response.set_redirection(m_root + "/" + bookName + "/" + kiwix::urlEncode(entry.getPath()));
|
||||
return response;
|
||||
} catch(kiwix::NoEntry& e) {
|
||||
return build_404(request, bookName);
|
||||
}
|
||||
}
|
||||
|
||||
Response InternalServer::handle_catalog(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_catalog");
|
||||
}
|
||||
|
||||
std::string host;
|
||||
std::string url;
|
||||
try {
|
||||
host = request.get_header("Host");
|
||||
url = request.get_url_part(1);
|
||||
} catch (const std::out_of_range&) {
|
||||
return build_404(request, "");
|
||||
}
|
||||
|
||||
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
|
||||
return build_404(request, "");
|
||||
}
|
||||
|
||||
auto response = get_default_response();
|
||||
response.set_compress(true);
|
||||
if (url == "searchdescription.xml") {
|
||||
response.set_template(RESOURCE::opensearchdescription_xml, get_default_data());
|
||||
response.set_mimeType("application/opensearchdescription+xml");
|
||||
return response;
|
||||
}
|
||||
|
||||
zim::Uuid uuid;
|
||||
kiwix::OPDSDumper opdsDumper;
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setSearchDescriptionUrl("catalog/searchdescription.xml");
|
||||
opdsDumper.setId(kiwix::to_string(uuid));
|
||||
opdsDumper.setLibrary(mp_library);
|
||||
response.set_mimeType("application/atom+xml;profile=opds-catalog;kind=acquisition; charset=utf-8");
|
||||
std::vector<std::string> bookIdsToDump;
|
||||
if (url == "root.xml") {
|
||||
opdsDumper.setTitle("All zims");
|
||||
uuid = zim::Uuid::generate(host);
|
||||
bookIdsToDump = mp_library->filter(kiwix::Filter().valid(true).local(true).remote(true));
|
||||
} else if (url == "search") {
|
||||
std::string query;
|
||||
std::string language;
|
||||
std::vector<std::string> tags;
|
||||
std::vector<std::string> noTags;
|
||||
size_t count(10);
|
||||
size_t startIndex(0);
|
||||
try {
|
||||
query = request.get_argument("q");
|
||||
} catch (const std::out_of_range&) {}
|
||||
try {
|
||||
language = request.get_argument("lang");
|
||||
} catch (const std::out_of_range&) {}
|
||||
try {
|
||||
count = extractFromString<unsigned long>(request.get_argument("count"));
|
||||
} catch (...) {}
|
||||
try {
|
||||
startIndex = extractFromString<unsigned long>(request.get_argument("start"));
|
||||
} catch (...) {}
|
||||
try {
|
||||
tags = kiwix::split(request.get_argument("notag"), ";");
|
||||
} catch (...) {}
|
||||
try {
|
||||
noTags = kiwix::split(request.get_argument("notag"), ";");
|
||||
} catch (...) {}
|
||||
opdsDumper.setTitle("Search result for " + query);
|
||||
uuid = zim::Uuid::generate();
|
||||
bookIdsToDump = mp_library->filter(
|
||||
kiwix::Filter().valid(true).local(true).remote(true)
|
||||
.query(query)
|
||||
.lang(language)
|
||||
.acceptTags(tags)
|
||||
.rejectTags(noTags)
|
||||
);
|
||||
auto totalResults = bookIdsToDump.size();
|
||||
bookIdsToDump.erase(bookIdsToDump.begin(), bookIdsToDump.begin()+startIndex);
|
||||
if (count>0 && bookIdsToDump.size() > count) {
|
||||
bookIdsToDump.resize(count);
|
||||
}
|
||||
opdsDumper.setOpenSearchInfo(totalResults, startIndex, bookIdsToDump.size());
|
||||
}
|
||||
|
||||
response.set_content(opdsDumper.dumpOPDSFeed(bookIdsToDump));
|
||||
return response;
|
||||
}
|
||||
|
||||
Response InternalServer::handle_content(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_content\n");
|
||||
}
|
||||
|
||||
std::string baseUrl;
|
||||
std::string content;
|
||||
std::string mimeType;
|
||||
|
||||
kiwix::Entry entry;
|
||||
|
||||
std::string bookName;
|
||||
try {
|
||||
bookName = request.get_url_part(0);
|
||||
} catch (const std::out_of_range& e) {
|
||||
return build_homepage(request);
|
||||
}
|
||||
if (bookName.empty())
|
||||
return build_homepage(request);
|
||||
|
||||
std::string bookId;
|
||||
std::shared_ptr<Reader> reader;
|
||||
try {
|
||||
bookId = mp_nameMapper->getIdForName(bookName);
|
||||
reader = mp_library->getReaderById(bookId);
|
||||
} catch (const std::out_of_range& e) {
|
||||
return build_404(request, bookName);
|
||||
}
|
||||
|
||||
if (reader == nullptr) {
|
||||
return build_404(request, bookName);
|
||||
}
|
||||
|
||||
auto urlStr = request.get_url().substr(bookName.size()+1);
|
||||
if (urlStr[0] == '/') {
|
||||
urlStr = urlStr.substr(1);
|
||||
}
|
||||
|
||||
try {
|
||||
entry = reader->getEntryFromPath(urlStr);
|
||||
if (entry.isRedirect() || urlStr.empty()) {
|
||||
// If urlStr is empty, we want to mainPage.
|
||||
// We must do a redirection to the real page.
|
||||
entry = entry.getFinalEntry();
|
||||
auto response = get_default_response();
|
||||
response.set_redirection(m_root + "/" + bookName + "/" +
|
||||
kiwix::urlEncode(entry.getPath()));
|
||||
return response;
|
||||
}
|
||||
} catch(kiwix::NoEntry& e) {
|
||||
if (m_verbose.load())
|
||||
printf("Failed to find %s\n", urlStr.c_str());
|
||||
|
||||
return build_404(request, bookName);
|
||||
}
|
||||
|
||||
try {
|
||||
mimeType = entry.getMimetype();
|
||||
} catch (exception& e) {
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
|
||||
if (m_verbose.load()) {
|
||||
printf("Found %s\n", urlStr.c_str());
|
||||
printf("mimeType: %s\n", mimeType.c_str());
|
||||
}
|
||||
|
||||
if (mimeType.find("text/") != string::npos
|
||||
|| mimeType.find("application/javascript") != string::npos
|
||||
|| mimeType.find("application/json") != string::npos) {
|
||||
zim::Blob raw_content = entry.getBlob();
|
||||
content = string(raw_content.data(), raw_content.size());
|
||||
auto response = get_default_response();
|
||||
|
||||
if (mimeType.find("text/html") != string::npos)
|
||||
response.set_taskbar(bookName, reader->getTitle());
|
||||
|
||||
response.set_mimeType(mimeType);
|
||||
response.set_content(content);
|
||||
response.set_compress(true);
|
||||
response.set_cache(true);
|
||||
return response;
|
||||
} else {
|
||||
int range_len;
|
||||
if (request.get_range().second == -1) {
|
||||
range_len = entry.getSize() - request.get_range().first;
|
||||
} else {
|
||||
range_len = request.get_range().second - request.get_range().first;
|
||||
}
|
||||
auto response = get_default_response();
|
||||
response.set_entry(entry);
|
||||
response.set_mimeType(mimeType);
|
||||
response.set_range_first(request.get_range().first);
|
||||
response.set_range_len(range_len);
|
||||
response.set_cache(true);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
127
src/server/byte_range.cpp
Normal file
127
src/server/byte_range.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2020 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#include "byte_range.h"
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
namespace {
|
||||
|
||||
ByteRange parseByteRange(const std::string& rangeStr)
|
||||
{
|
||||
std::istringstream iss(rangeStr);
|
||||
|
||||
int64_t start, end = INT64_MAX;
|
||||
if (iss >> start) {
|
||||
if ( start < 0 ) {
|
||||
if ( iss.eof() )
|
||||
return ByteRange(-start);
|
||||
} else {
|
||||
char c;
|
||||
if (iss >> c && c=='-') {
|
||||
iss >> end; // if this fails, end is not modified, which is OK
|
||||
if (iss.eof() && start <= end)
|
||||
return ByteRange(ByteRange::PARSED, start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ByteRange(ByteRange::INVALID, 0, INT64_MAX);
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
ByteRange::ByteRange()
|
||||
: kind_(NONE)
|
||||
, first_(0)
|
||||
, last_(INT64_MAX)
|
||||
{}
|
||||
|
||||
ByteRange::ByteRange(Kind kind, int64_t first, int64_t last)
|
||||
: kind_(kind)
|
||||
, first_(first)
|
||||
, last_(last)
|
||||
{
|
||||
assert(kind != NONE);
|
||||
assert(first >= 0);
|
||||
assert(last >= first || (first == 0 && last == -1));
|
||||
}
|
||||
|
||||
ByteRange::ByteRange(int64_t suffix_length)
|
||||
: kind_(PARSED)
|
||||
, first_(-suffix_length)
|
||||
, last_(INT64_MAX)
|
||||
{
|
||||
assert(suffix_length > 0);
|
||||
}
|
||||
|
||||
int64_t ByteRange::first() const
|
||||
{
|
||||
assert(kind_ > PARSED);
|
||||
return first_;
|
||||
}
|
||||
|
||||
int64_t ByteRange::last() const
|
||||
{
|
||||
assert(kind_ > PARSED);
|
||||
return last_;
|
||||
}
|
||||
|
||||
int64_t ByteRange::length() const
|
||||
{
|
||||
assert(kind_ > PARSED);
|
||||
return last_ + 1 - first_;
|
||||
}
|
||||
|
||||
ByteRange ByteRange::parse(const std::string& rangeStr)
|
||||
{
|
||||
const std::string byteUnitSpec("bytes=");
|
||||
if ( ! kiwix::startsWith(rangeStr, byteUnitSpec) )
|
||||
return ByteRange(INVALID, 0, INT64_MAX);
|
||||
|
||||
return parseByteRange(rangeStr.substr(byteUnitSpec.size()));
|
||||
}
|
||||
|
||||
ByteRange ByteRange::resolve(int64_t contentSize) const
|
||||
{
|
||||
if ( kind() == NONE )
|
||||
return ByteRange(RESOLVED_FULL_CONTENT, 0, contentSize-1);
|
||||
|
||||
if ( kind() == INVALID )
|
||||
return ByteRange(RESOLVED_UNSATISFIABLE, 0, contentSize-1);
|
||||
|
||||
const int64_t resolved_first = first_ < 0
|
||||
? std::max(int64_t(0), contentSize + first_)
|
||||
: first_;
|
||||
|
||||
const int64_t resolved_last = std::min(contentSize-1, last_);
|
||||
|
||||
if ( resolved_first > resolved_last )
|
||||
return ByteRange(RESOLVED_UNSATISFIABLE, 0, contentSize-1);
|
||||
|
||||
return ByteRange(RESOLVED_PARTIAL_CONTENT, resolved_first, resolved_last);
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
86
src/server/byte_range.h
Normal file
86
src/server/byte_range.h
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2020 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#ifndef KIWIXLIB_SERVER_BYTE_RANGE_H
|
||||
#define KIWIXLIB_SERVER_BYTE_RANGE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
class ByteRange
|
||||
{
|
||||
public: // types
|
||||
// ByteRange is parsed in a request, then it must be resolved (taking
|
||||
// into account the actual size of the requested resource) before
|
||||
// being applied in the response.
|
||||
// The Kind enum represents possible states in such a lifecycle.
|
||||
enum Kind {
|
||||
// The request is not a range request (no Range header)
|
||||
NONE,
|
||||
|
||||
// The value of the Range header is not a valid continuous
|
||||
// range. Note that a valid (according to RFC7233) sequence of multiple
|
||||
// byte ranges is considered invalid in the current implementation
|
||||
// (i.e. only single-range partial requests are supported).
|
||||
INVALID,
|
||||
|
||||
// This byte-range has been successfully parsed from the request
|
||||
PARSED,
|
||||
|
||||
// This is a response to a regular (non-range) request
|
||||
RESOLVED_FULL_CONTENT,
|
||||
|
||||
// The range request is invalid or unsatisfiable
|
||||
RESOLVED_UNSATISFIABLE,
|
||||
|
||||
// This is a response to a (satisfiable) range request
|
||||
RESOLVED_PARTIAL_CONTENT,
|
||||
};
|
||||
|
||||
public: // functions
|
||||
// Constructs a ByteRange object of NONE kind
|
||||
ByteRange();
|
||||
|
||||
// Constructs a ByteRange object of the given kind (except NONE)
|
||||
ByteRange(Kind kind, int64_t first, int64_t last);
|
||||
|
||||
// Constructs a ByteRange object of PARSED kind corresponding to a
|
||||
// range request of the form "Range: bytes=-suffix_length"
|
||||
explicit ByteRange(int64_t suffix_length);
|
||||
|
||||
Kind kind() const { return kind_; }
|
||||
int64_t first() const;
|
||||
int64_t last() const;
|
||||
int64_t length() const;
|
||||
|
||||
static ByteRange parse(const std::string& rangeStr);
|
||||
ByteRange resolve(int64_t contentSize) const;
|
||||
|
||||
private: // data
|
||||
Kind kind_;
|
||||
int64_t first_;
|
||||
int64_t last_;
|
||||
};
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif //KIWIXLIB_SERVER_BYTE_RANGE_H
|
||||
135
src/server/etag.cpp
Normal file
135
src/server/etag.cpp
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2020 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#include "etag.h"
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
namespace {
|
||||
|
||||
// Characters in the options part of the ETag could in principle be picked up
|
||||
// from the latin alphabet in natural order (the character corresponding to
|
||||
// ETag::Option opt would be 'a'+opt; that would somewhat simplify the code in
|
||||
// this file). However it is better to have some mnemonics in the option names,
|
||||
// hence below variable: all_options[opt] corresponds to the character going
|
||||
// into the ETag for ETag::Option opt.
|
||||
// IMPORTANT: The characters in all_options must come in sorted order (so that
|
||||
// IMPORTANT: isValidOptionsString() works correctly).
|
||||
const char all_options[] = "cz";
|
||||
|
||||
static_assert(ETag::OPTION_COUNT == sizeof(all_options) - 1, "");
|
||||
|
||||
bool isValidServerId(const std::string& s)
|
||||
{
|
||||
return !s.empty() && s.find_first_of("\"/") == std::string::npos;
|
||||
}
|
||||
|
||||
bool isSubsequenceOf(const std::string& s, const std::string& sortedString)
|
||||
{
|
||||
std::string::size_type i = 0;
|
||||
for ( const char c : s )
|
||||
{
|
||||
const std::string::size_type j = sortedString.find(c, i);
|
||||
if ( j == std::string::npos )
|
||||
return false;
|
||||
i = j+1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isValidOptionsString(const std::string& s)
|
||||
{
|
||||
return isSubsequenceOf(s, all_options);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
void ETag::set_option(Option opt)
|
||||
{
|
||||
if ( ! get_option(opt) )
|
||||
{
|
||||
m_options.push_back(all_options[opt]);
|
||||
std::sort(m_options.begin(), m_options.end());
|
||||
}
|
||||
}
|
||||
|
||||
bool ETag::get_option(Option opt) const
|
||||
{
|
||||
return m_options.find(all_options[opt]) != std::string::npos;
|
||||
}
|
||||
|
||||
std::string ETag::get_etag() const
|
||||
{
|
||||
if ( m_serverId.empty() )
|
||||
return std::string();
|
||||
|
||||
return "\"" + m_serverId + "/" + m_options + "\"";
|
||||
}
|
||||
|
||||
ETag::ETag(const std::string& serverId, const std::string& options)
|
||||
{
|
||||
if ( isValidServerId(serverId) && isValidOptionsString(options) )
|
||||
{
|
||||
m_serverId = serverId;
|
||||
m_options = options;
|
||||
}
|
||||
}
|
||||
|
||||
ETag ETag::parse(std::string s)
|
||||
{
|
||||
if ( kiwix::startsWith("W/", s) )
|
||||
s = s.substr(2);
|
||||
|
||||
if ( s.front() != '"' || s.back() != '"' )
|
||||
return ETag();
|
||||
|
||||
s = s.substr(1, s.size()-2);
|
||||
|
||||
const std::string::size_type i = s.find('/');
|
||||
if ( i == std::string::npos )
|
||||
return ETag();
|
||||
|
||||
return ETag(s.substr(0, i), s.substr(i+1));
|
||||
}
|
||||
|
||||
ETag ETag::match(const std::string& etags, const std::string& server_id)
|
||||
{
|
||||
std::istringstream ss(etags);
|
||||
std::string etag_str;
|
||||
while ( ss >> etag_str )
|
||||
{
|
||||
if ( etag_str.back() == ',' )
|
||||
etag_str.pop_back();
|
||||
|
||||
const ETag etag = parse(etag_str);
|
||||
if ( etag && etag.m_serverId == server_id )
|
||||
return etag;
|
||||
}
|
||||
|
||||
return ETag();
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
85
src/server/etag.h
Normal file
85
src/server/etag.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2020 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#ifndef KIWIXLIB_SERVER_ETAG_H
|
||||
#define KIWIXLIB_SERVER_ETAG_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
// The ETag string used by Kiwix server (more precisely, its value inside the
|
||||
// double quotes) consists of two parts:
|
||||
//
|
||||
// 1. ServerId - The string obtained on server start up
|
||||
//
|
||||
// 2. Options - Zero or more characters encoding the values of some of the
|
||||
// headers of the response
|
||||
//
|
||||
// The two parts are separated with a slash (/) symbol (which is always present,
|
||||
// even when the the options part is empty). Neither portion of a Kiwix ETag
|
||||
// may contain the slash symbol.
|
||||
// Examples of valid Kiwix server ETags (including the double quotes):
|
||||
//
|
||||
// "abcdefghijklmn/"
|
||||
// "1234567890/z"
|
||||
// "1234567890/cz"
|
||||
//
|
||||
// The options part of the Kiwix ETag allows to correctly set the required
|
||||
// headers when responding to a conditional If-None-Match request with a 304
|
||||
// (Not Modified) response without following the full code path that would
|
||||
// discover the necessary options.
|
||||
|
||||
class ETag
|
||||
{
|
||||
public: // types
|
||||
enum Option {
|
||||
CACHEABLE_ENTITY,
|
||||
COMPRESSED_CONTENT,
|
||||
OPTION_COUNT
|
||||
};
|
||||
|
||||
public: // functions
|
||||
ETag() {}
|
||||
|
||||
void set_server_id(const std::string& id) { m_serverId = id; }
|
||||
void set_option(Option opt);
|
||||
|
||||
explicit operator bool() const { return !m_serverId.empty(); }
|
||||
|
||||
bool get_option(Option opt) const;
|
||||
std::string get_etag() const;
|
||||
|
||||
|
||||
static ETag match(const std::string& etags, const std::string& server_id);
|
||||
|
||||
private: // functions
|
||||
ETag(const std::string& serverId, const std::string& options);
|
||||
|
||||
static ETag parse(std::string s);
|
||||
|
||||
private: // data
|
||||
std::string m_serverId;
|
||||
std::string m_options;
|
||||
};
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif // KIWIXLIB_SERVER_ETAG_H
|
||||
810
src/server/internalServer.cpp
Normal file
810
src/server/internalServer.cpp
Normal file
@@ -0,0 +1,810 @@
|
||||
/*
|
||||
* Copyright 2019 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "internalServer.h"
|
||||
|
||||
#ifdef __FreeBSD__
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
# if !defined(__MINGW32__) && (_MSC_VER < 1600)
|
||||
# include "stdint4win.h"
|
||||
# endif
|
||||
# include <winsock2.h>
|
||||
# include <ws2tcpip.h>
|
||||
# ifdef __GNUC__
|
||||
// inet_pton is not declared in mingw, even if the function exists.
|
||||
extern "C" {
|
||||
WINSOCK_API_LINKAGE INT WSAAPI inet_pton( INT Family, PCSTR pszAddrString, PVOID pAddrBuf);
|
||||
}
|
||||
# endif
|
||||
typedef UINT64 uint64_t;
|
||||
typedef UINT16 uint16_t;
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
#include "microhttpd_wrapper.h"
|
||||
}
|
||||
|
||||
#include "tools/otherTools.h"
|
||||
#include "tools/pathTools.h"
|
||||
#include "tools/regexTools.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include "library.h"
|
||||
#include "name_mapper.h"
|
||||
#include "entry.h"
|
||||
#include "searcher.h"
|
||||
#include "search_renderer.h"
|
||||
#include "opds_dumper.h"
|
||||
|
||||
#include <zim/uuid.h>
|
||||
|
||||
#include <mustache.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include "kiwixlib-resources.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
# include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
#include "request_context.h"
|
||||
#include "response.h"
|
||||
|
||||
#define MAX_SEARCH_LEN 140
|
||||
#define KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE 100
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
inline std::string normalizeRootUrl(std::string rootUrl)
|
||||
{
|
||||
while ( !rootUrl.empty() && rootUrl.back() == '/' )
|
||||
rootUrl.pop_back();
|
||||
|
||||
while ( !rootUrl.empty() && rootUrl.front() == '/' )
|
||||
rootUrl = rootUrl.substr(1);
|
||||
return rootUrl.empty() ? rootUrl : "/" + rootUrl;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
static IdNameMapper defaultNameMapper;
|
||||
|
||||
static MHD_Result staticHandlerCallback(void* cls,
|
||||
struct MHD_Connection* connection,
|
||||
const char* url,
|
||||
const char* method,
|
||||
const char* version,
|
||||
const char* upload_data,
|
||||
size_t* upload_data_size,
|
||||
void** cont_cls);
|
||||
|
||||
|
||||
InternalServer::InternalServer(Library* library,
|
||||
NameMapper* nameMapper,
|
||||
std::string addr,
|
||||
int port,
|
||||
std::string root,
|
||||
int nbThreads,
|
||||
bool verbose,
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton,
|
||||
bool blockExternalLinks) :
|
||||
m_addr(addr),
|
||||
m_port(port),
|
||||
m_root(normalizeRootUrl(root)),
|
||||
m_nbThreads(nbThreads),
|
||||
m_verbose(verbose),
|
||||
m_withTaskbar(withTaskbar),
|
||||
m_withLibraryButton(withLibraryButton),
|
||||
m_blockExternalLinks(blockExternalLinks),
|
||||
mp_daemon(nullptr),
|
||||
mp_library(library),
|
||||
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper)
|
||||
{}
|
||||
|
||||
bool InternalServer::start() {
|
||||
#ifdef _WIN32
|
||||
int flags = MHD_USE_SELECT_INTERNALLY;
|
||||
#else
|
||||
int flags = MHD_USE_POLL_INTERNALLY;
|
||||
#endif
|
||||
if (m_verbose.load())
|
||||
flags |= MHD_USE_DEBUG;
|
||||
|
||||
|
||||
struct sockaddr_in sockAddr;
|
||||
memset(&sockAddr, 0, sizeof(sockAddr));
|
||||
sockAddr.sin_family = AF_INET;
|
||||
sockAddr.sin_port = htons(m_port);
|
||||
if (m_addr.empty()) {
|
||||
if (0 != INADDR_ANY)
|
||||
sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
} else {
|
||||
if (inet_pton(AF_INET, m_addr.c_str(), &(sockAddr.sin_addr.s_addr)) == 0) {
|
||||
std::cerr << "Ip address " << m_addr << " is not a valid ip address" << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
mp_daemon = MHD_start_daemon(flags,
|
||||
m_port,
|
||||
NULL,
|
||||
NULL,
|
||||
&staticHandlerCallback,
|
||||
this,
|
||||
MHD_OPTION_SOCK_ADDR, &sockAddr,
|
||||
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
|
||||
MHD_OPTION_END);
|
||||
if (mp_daemon == nullptr) {
|
||||
std::cerr << "Unable to instantiate the HTTP daemon. The port " << m_port
|
||||
<< " is maybe already occupied or need more permissions to be open. "
|
||||
"Please try as root or with a port number higher or equal to 1024."
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
auto server_start_time = std::chrono::system_clock::now().time_since_epoch();
|
||||
m_server_id = kiwix::to_string(server_start_time.count());
|
||||
m_library_id = m_server_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
void InternalServer::stop()
|
||||
{
|
||||
MHD_stop_daemon(mp_daemon);
|
||||
}
|
||||
|
||||
static MHD_Result staticHandlerCallback(void* cls,
|
||||
struct MHD_Connection* connection,
|
||||
const char* url,
|
||||
const char* method,
|
||||
const char* version,
|
||||
const char* upload_data,
|
||||
size_t* upload_data_size,
|
||||
void** cont_cls)
|
||||
{
|
||||
InternalServer* _this = static_cast<InternalServer*>(cls);
|
||||
|
||||
return _this->handlerCallback(connection,
|
||||
url,
|
||||
method,
|
||||
version,
|
||||
upload_data,
|
||||
upload_data_size,
|
||||
cont_cls);
|
||||
}
|
||||
|
||||
MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
const char* url,
|
||||
const char* method,
|
||||
const char* version,
|
||||
const char* upload_data,
|
||||
size_t* upload_data_size,
|
||||
void** cont_cls)
|
||||
{
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
if (m_verbose.load() ) {
|
||||
printf("======================\n");
|
||||
printf("Requesting : \n");
|
||||
printf("full_url : %s\n", url);
|
||||
}
|
||||
RequestContext request(connection, m_root, url, method, version);
|
||||
|
||||
if (m_verbose.load() ) {
|
||||
request.print_debug_info();
|
||||
}
|
||||
/* Unexpected method */
|
||||
if (request.get_method() != RequestMethod::GET
|
||||
&& request.get_method() != RequestMethod::POST
|
||||
&& request.get_method() != RequestMethod::HEAD) {
|
||||
printf("Reject request because of unhandled request method.\n");
|
||||
printf("----------------------\n");
|
||||
return MHD_NO;
|
||||
}
|
||||
|
||||
auto response = handle_request(request);
|
||||
|
||||
if (response->getReturnCode() == MHD_HTTP_INTERNAL_SERVER_ERROR) {
|
||||
printf("========== INTERNAL ERROR !! ============\n");
|
||||
if (!m_verbose.load()) {
|
||||
printf("Requesting : \n");
|
||||
printf("full_url : %s\n", url);
|
||||
request.print_debug_info();
|
||||
}
|
||||
}
|
||||
|
||||
if (response->getReturnCode() == MHD_HTTP_OK && !etag_not_needed(request))
|
||||
response->set_server_id(m_server_id);
|
||||
|
||||
auto ret = response->send(request, connection);
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(end_time - start_time);
|
||||
if (m_verbose.load()) {
|
||||
printf("Request time : %fs\n", time_span.count());
|
||||
printf("----------------------\n");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& request)
|
||||
{
|
||||
try {
|
||||
if (! request.is_valid_url())
|
||||
return Response::build_404(*this, request, "", "");
|
||||
|
||||
const ETag etag = get_matching_if_none_match_etag(request);
|
||||
if ( etag )
|
||||
return Response::build_304(*this, etag);
|
||||
|
||||
if (startsWith(request.get_url(), "/skin/"))
|
||||
return handle_skin(request);
|
||||
|
||||
if (startsWith(request.get_url(), "/catalog/"))
|
||||
return handle_catalog(request);
|
||||
|
||||
if (request.get_url() == "/meta")
|
||||
return handle_meta(request);
|
||||
|
||||
if (request.get_url() == "/search")
|
||||
return handle_search(request);
|
||||
|
||||
if (request.get_url() == "/suggest")
|
||||
return handle_suggest(request);
|
||||
|
||||
if (request.get_url() == "/random")
|
||||
return handle_random(request);
|
||||
|
||||
if (request.get_url() == "/catch/external")
|
||||
return handle_captured_external(request);
|
||||
|
||||
return handle_content(request);
|
||||
} catch (std::exception& e) {
|
||||
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
|
||||
return Response::build_500(*this, e.what());
|
||||
} catch (...) {
|
||||
fprintf(stderr, "===== Unhandled unknown error\n");
|
||||
return Response::build_500(*this, "Unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
MustacheData InternalServer::get_default_data() const
|
||||
{
|
||||
MustacheData data;
|
||||
data.set("root", m_root);
|
||||
return data;
|
||||
}
|
||||
|
||||
bool InternalServer::etag_not_needed(const RequestContext& request) const
|
||||
{
|
||||
const std::string url = request.get_url();
|
||||
return kiwix::startsWith(url, "/catalog")
|
||||
|| url == "/search"
|
||||
|| url == "/suggest"
|
||||
|| url == "/random"
|
||||
|| url == "/catch/external";
|
||||
}
|
||||
|
||||
ETag
|
||||
InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const
|
||||
{
|
||||
try {
|
||||
const std::string etag_list = r.get_header(MHD_HTTP_HEADER_IF_NONE_MATCH);
|
||||
return ETag::match(etag_list, m_server_id);
|
||||
} catch (const std::out_of_range&) {
|
||||
return ETag();
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
|
||||
{
|
||||
return ContentResponse::build(*this, RESOURCE::templates::index_html, get_default_data(), "text/html; charset=utf-8", true);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_meta(const RequestContext& request)
|
||||
{
|
||||
std::string bookName;
|
||||
std::string bookId;
|
||||
std::string meta_name;
|
||||
std::shared_ptr<Reader> reader;
|
||||
try {
|
||||
bookName = request.get_argument("content");
|
||||
bookId = mp_nameMapper->getIdForName(bookName);
|
||||
meta_name = request.get_argument("name");
|
||||
reader = mp_library->getReaderById(bookId);
|
||||
} catch (const std::out_of_range& e) {
|
||||
return Response::build_404(*this, request, bookName, "");
|
||||
}
|
||||
|
||||
if (reader == nullptr) {
|
||||
return Response::build_404(*this, request, bookName, "");
|
||||
}
|
||||
|
||||
std::string content;
|
||||
std::string mimeType = "text";
|
||||
|
||||
if (meta_name == "title") {
|
||||
content = reader->getTitle();
|
||||
} else if (meta_name == "description") {
|
||||
content = reader->getDescription();
|
||||
} else if (meta_name == "language") {
|
||||
content = reader->getLanguage();
|
||||
} else if (meta_name == "name") {
|
||||
content = reader->getName();
|
||||
} else if (meta_name == "tags") {
|
||||
content = reader->getTags();
|
||||
} else if (meta_name == "date") {
|
||||
content = reader->getDate();
|
||||
} else if (meta_name == "creator") {
|
||||
content = reader->getCreator();
|
||||
} else if (meta_name == "publisher") {
|
||||
content = reader->getPublisher();
|
||||
} else if (meta_name == "favicon") {
|
||||
reader->getFavicon(content, mimeType);
|
||||
} else {
|
||||
return Response::build_404(*this, request, bookName, "");
|
||||
}
|
||||
|
||||
auto response = ContentResponse::build(*this, content, mimeType);
|
||||
response->set_cacheable();
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_suggest\n");
|
||||
}
|
||||
|
||||
std::string content;
|
||||
std::string mimeType;
|
||||
unsigned int maxSuggestionCount = 10;
|
||||
unsigned int suggestionCount = 0;
|
||||
|
||||
std::string bookName;
|
||||
std::string bookId;
|
||||
std::string term;
|
||||
std::shared_ptr<Reader> reader;
|
||||
try {
|
||||
bookName = request.get_argument("content");
|
||||
bookId = mp_nameMapper->getIdForName(bookName);
|
||||
term = request.get_argument("term");
|
||||
reader = mp_library->getReaderById(bookId);
|
||||
} catch (const std::out_of_range&) {
|
||||
return Response::build_404(*this, request, bookName, "");
|
||||
}
|
||||
|
||||
if (m_verbose.load()) {
|
||||
printf("Searching suggestions for: \"%s\"\n", term.c_str());
|
||||
}
|
||||
|
||||
MustacheData results{MustacheData::type::list};
|
||||
|
||||
bool first = true;
|
||||
if (reader != nullptr) {
|
||||
/* Get the suggestions */
|
||||
SuggestionsList_t suggestions;
|
||||
reader->searchSuggestionsSmart(term, maxSuggestionCount, suggestions);
|
||||
for(auto& suggestion:suggestions) {
|
||||
MustacheData result;
|
||||
result.set("label", suggestion.getTitle());
|
||||
|
||||
if (suggestion.hasSnippet()) {
|
||||
result.set("label", suggestion.getSnippet());
|
||||
}
|
||||
|
||||
result.set("value", suggestion.getTitle());
|
||||
result.set("kind", "path");
|
||||
result.set("path", suggestion.getPath());
|
||||
result.set("first", first);
|
||||
first = false;
|
||||
results.push_back(result);
|
||||
suggestionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Propose the fulltext search if possible */
|
||||
if (reader->hasFulltextIndex()) {
|
||||
MustacheData result;
|
||||
result.set("label", "containing '" + term + "'...");
|
||||
result.set("value", term + " ");
|
||||
result.set("kind", "pattern");
|
||||
result.set("first", first);
|
||||
results.push_back(result);
|
||||
}
|
||||
|
||||
auto data = get_default_data();
|
||||
data.set("suggestions", results);
|
||||
|
||||
auto response = ContentResponse::build(*this, RESOURCE::templates::suggestion_json, data, "application/json; charset=utf-8");
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_skin\n");
|
||||
}
|
||||
|
||||
auto resourceName = request.get_url().substr(1);
|
||||
try {
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
getResource(resourceName),
|
||||
getMimeTypeForFile(resourceName));
|
||||
response->set_cacheable();
|
||||
return std::move(response);
|
||||
} catch (const ResourceNotFound& e) {
|
||||
return Response::build_404(*this, request, "", "");
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_search\n");
|
||||
}
|
||||
|
||||
std::string bookName;
|
||||
std::string bookId;
|
||||
try {
|
||||
bookName = request.get_argument("content");
|
||||
bookId = mp_nameMapper->getIdForName(bookName);
|
||||
} catch (const std::out_of_range&) {}
|
||||
|
||||
std::string patternString;
|
||||
try {
|
||||
patternString = request.get_argument("pattern");
|
||||
} catch (const std::out_of_range&) {}
|
||||
|
||||
/* Retrive geo search */
|
||||
bool has_geo_query = false;
|
||||
float latitude = 0;
|
||||
float longitude = 0;
|
||||
float distance = 0;
|
||||
try {
|
||||
latitude = request.get_argument<float>("latitude");
|
||||
longitude = request.get_argument<float>("longitude");
|
||||
distance = request.get_argument<float>("distance");
|
||||
has_geo_query = true;
|
||||
} catch(const std::out_of_range&) {}
|
||||
catch(const std::invalid_argument&) {}
|
||||
|
||||
std::shared_ptr<Reader> reader(nullptr);
|
||||
try {
|
||||
reader = mp_library->getReaderById(bookId);
|
||||
} catch (const std::out_of_range&) {}
|
||||
|
||||
/* Make the search */
|
||||
if ( (!reader && !bookName.empty())
|
||||
|| (patternString.empty() && ! has_geo_query) ) {
|
||||
auto data = get_default_data();
|
||||
data.set("pattern", encodeDiples(patternString));
|
||||
auto response = ContentResponse::build(*this, RESOURCE::templates::no_search_result_html, data, "text/html; charset=utf-8");
|
||||
response->set_taskbar(bookName, reader ? reader->getTitle() : "");
|
||||
response->set_code(MHD_HTTP_NOT_FOUND);
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
Searcher searcher;
|
||||
if (reader) {
|
||||
searcher.add_reader(reader.get());
|
||||
} else {
|
||||
for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
|
||||
auto currentReader = mp_library->getReaderById(bookId);
|
||||
if (currentReader) {
|
||||
searcher.add_reader(currentReader.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto start = 0;
|
||||
try {
|
||||
start = request.get_argument<unsigned int>("start");
|
||||
} catch (const std::exception&) {}
|
||||
|
||||
auto pageLength = 25;
|
||||
try {
|
||||
pageLength = request.get_argument<unsigned int>("pageLength");
|
||||
} catch (const std::exception&) {}
|
||||
if (pageLength > MAX_SEARCH_LEN) {
|
||||
pageLength = MAX_SEARCH_LEN;
|
||||
}
|
||||
if (pageLength == 0) {
|
||||
pageLength = 25;
|
||||
}
|
||||
|
||||
auto end = start + pageLength;
|
||||
|
||||
/* Get the results */
|
||||
try {
|
||||
if (patternString.empty()) {
|
||||
searcher.geo_search(latitude, longitude, distance,
|
||||
start, end, m_verbose.load());
|
||||
} else {
|
||||
searcher.search(patternString,
|
||||
start, end, m_verbose.load());
|
||||
}
|
||||
SearchRenderer renderer(&searcher, mp_nameMapper);
|
||||
renderer.setSearchPattern(patternString);
|
||||
renderer.setSearchContent(bookName);
|
||||
renderer.setProtocolPrefix(m_root + "/");
|
||||
renderer.setSearchProtocolPrefix(m_root + "/search?");
|
||||
renderer.setPageLength(pageLength);
|
||||
auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8");
|
||||
response->set_taskbar(bookName, reader ? reader->getTitle() : "");
|
||||
|
||||
return std::move(response);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << e.what() << std::endl;
|
||||
return Response::build_500(*this, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_random\n");
|
||||
}
|
||||
|
||||
std::string bookName;
|
||||
std::string bookId;
|
||||
std::shared_ptr<Reader> reader;
|
||||
try {
|
||||
bookName = request.get_argument("content");
|
||||
bookId = mp_nameMapper->getIdForName(bookName);
|
||||
reader = mp_library->getReaderById(bookId);
|
||||
} catch (const std::out_of_range&) {
|
||||
return Response::build_404(*this, request, bookName, "");
|
||||
}
|
||||
|
||||
if (reader == nullptr) {
|
||||
return Response::build_404(*this, request, bookName, "");
|
||||
}
|
||||
|
||||
try {
|
||||
auto entry = reader->getRandomPage();
|
||||
return build_redirect(bookName, entry.getFinalEntry());
|
||||
} catch(kiwix::NoEntry& e) {
|
||||
return Response::build_404(*this, request, bookName, "");
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_captured_external(const RequestContext& request)
|
||||
{
|
||||
std::string source = "";
|
||||
try {
|
||||
source = kiwix::urlDecode(request.get_argument("source"));
|
||||
} catch (const std::out_of_range& e) {}
|
||||
|
||||
if (source.empty())
|
||||
return Response::build_404(*this, request, "", "");
|
||||
|
||||
auto data = get_default_data();
|
||||
data.set("source", source);
|
||||
return ContentResponse::build(*this, RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_catalog");
|
||||
}
|
||||
|
||||
std::string host;
|
||||
std::string url;
|
||||
try {
|
||||
host = request.get_header("Host");
|
||||
url = request.get_url_part(1);
|
||||
} catch (const std::out_of_range&) {
|
||||
return Response::build_404(*this, request, "", "");
|
||||
}
|
||||
|
||||
if (url == "v2") {
|
||||
return handle_catalog_v2(request);
|
||||
}
|
||||
|
||||
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
|
||||
return Response::build_404(*this, request, "", "");
|
||||
}
|
||||
|
||||
if (url == "searchdescription.xml") {
|
||||
auto response = ContentResponse::build(*this, RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
zim::Uuid uuid;
|
||||
kiwix::OPDSDumper opdsDumper(mp_library);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(m_library_id);
|
||||
std::vector<std::string> bookIdsToDump;
|
||||
if (url == "root.xml") {
|
||||
uuid = zim::Uuid::generate(host);
|
||||
bookIdsToDump = mp_library->filter(kiwix::Filter().valid(true).local(true).remote(true));
|
||||
} else if (url == "search") {
|
||||
bookIdsToDump = search_catalog(request, opdsDumper);
|
||||
uuid = zim::Uuid::generate();
|
||||
}
|
||||
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()),
|
||||
"application/atom+xml; profile=opds-catalog; kind=acquisition; charset=utf-8");
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
Filter get_search_filter(const RequestContext& request)
|
||||
{
|
||||
auto filter = kiwix::Filter().valid(true).local(true);
|
||||
try {
|
||||
filter.query(request.get_argument("q"));
|
||||
} catch (const std::out_of_range&) {}
|
||||
try {
|
||||
filter.maxSize(request.get_argument<unsigned long>("maxsize"));
|
||||
} catch (...) {}
|
||||
try {
|
||||
filter.name(request.get_argument("name"));
|
||||
} catch (const std::out_of_range&) {}
|
||||
try {
|
||||
filter.category(request.get_argument("category"));
|
||||
} catch (const std::out_of_range&) {}
|
||||
try {
|
||||
filter.lang(request.get_argument("lang"));
|
||||
} catch (const std::out_of_range&) {}
|
||||
try {
|
||||
filter.acceptTags(kiwix::split(request.get_argument("tag"), ";"));
|
||||
} catch (...) {}
|
||||
try {
|
||||
filter.rejectTags(kiwix::split(request.get_argument("notag"), ";"));
|
||||
} catch (...) {}
|
||||
return filter;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
std::vector<T> subrange(const std::vector<T>& v, size_t s, size_t n)
|
||||
{
|
||||
const size_t e = std::min(v.size(), s+n);
|
||||
return std::vector<T>(v.begin()+std::min(v.size(), s), v.begin()+e);
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::vector<std::string>
|
||||
InternalServer::search_catalog(const RequestContext& request,
|
||||
kiwix::OPDSDumper& opdsDumper)
|
||||
{
|
||||
const auto filter = get_search_filter(request);
|
||||
const std::string q = filter.hasQuery()
|
||||
? filter.getQuery()
|
||||
: "<Empty query>";
|
||||
std::vector<std::string> bookIdsToDump = mp_library->filter(filter);
|
||||
const auto totalResults = bookIdsToDump.size();
|
||||
const size_t count = request.get_optional_param("count", 10UL);
|
||||
const size_t startIndex = request.get_optional_param("start", 0UL);
|
||||
bookIdsToDump = subrange(bookIdsToDump, startIndex, count);
|
||||
opdsDumper.setOpenSearchInfo(totalResults, startIndex, bookIdsToDump.size());
|
||||
return bookIdsToDump;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string get_book_name(const RequestContext& request)
|
||||
{
|
||||
try {
|
||||
return request.get_url_part(0);
|
||||
} catch (const std::out_of_range& e) {
|
||||
return std::string();
|
||||
}
|
||||
}
|
||||
|
||||
std::string searchSuggestionHTML(const std::string& searchURL, const std::string& pattern)
|
||||
{
|
||||
kainjow::mustache::mustache tmpl("Make a full text search for <a href=\"{{{searchURL}}}\">{{pattern}}</a>");
|
||||
MustacheData data;
|
||||
data.set("pattern", pattern);
|
||||
data.set("searchURL", searchURL);
|
||||
return (tmpl.render(data));
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::shared_ptr<Reader>
|
||||
InternalServer::get_reader(const std::string& bookName) const
|
||||
{
|
||||
std::shared_ptr<Reader> reader;
|
||||
try {
|
||||
const std::string bookId = mp_nameMapper->getIdForName(bookName);
|
||||
reader = mp_library->getReaderById(bookId);
|
||||
} catch (const std::out_of_range& e) {
|
||||
}
|
||||
return reader;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response>
|
||||
InternalServer::build_redirect(const std::string& bookName, const kiwix::Entry& entry) const
|
||||
{
|
||||
auto redirectUrl = m_root + "/" + bookName + "/" + kiwix::urlEncode(entry.getPath());
|
||||
return Response::build_redirect(*this, redirectUrl);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& request)
|
||||
{
|
||||
const std::string url = request.get_url();
|
||||
const std::string pattern = url.substr((url.find_last_of('/'))+1);
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_content\n");
|
||||
}
|
||||
|
||||
const std::string bookName = get_book_name(request);
|
||||
if (bookName.empty())
|
||||
return build_homepage(request);
|
||||
|
||||
const std::shared_ptr<Reader> reader = get_reader(bookName);
|
||||
if (reader == nullptr) {
|
||||
std::string searchURL = m_root+"/search?pattern="+pattern; // Make a full search on the entire library.
|
||||
const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern));
|
||||
|
||||
return Response::build_404(*this, request, bookName, "", details);
|
||||
}
|
||||
|
||||
auto urlStr = request.get_url().substr(bookName.size()+1);
|
||||
if (urlStr[0] == '/') {
|
||||
urlStr = urlStr.substr(1);
|
||||
}
|
||||
|
||||
try {
|
||||
auto entry = reader->getEntryFromPath(urlStr);
|
||||
if (entry.isRedirect() || urlStr.empty()) {
|
||||
// If urlStr is empty, we want to mainPage.
|
||||
// We must do a redirection to the real page.
|
||||
return build_redirect(bookName, entry.getFinalEntry());
|
||||
}
|
||||
auto response = ItemResponse::build(*this, request, entry.getZimEntry().getItem());
|
||||
try {
|
||||
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, reader->getTitle());
|
||||
} catch (std::bad_cast& e) {}
|
||||
|
||||
if (m_verbose.load()) {
|
||||
printf("Found %s\n", entry.getPath().c_str());
|
||||
printf("mimeType: %s\n", entry.getMimetype().c_str());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch(kiwix::NoEntry& e) {
|
||||
if (m_verbose.load())
|
||||
printf("Failed to find %s\n", urlStr.c_str());
|
||||
|
||||
std::string searchURL = m_root+"/search?content="+bookName+"&pattern="+pattern; // Make a search on this specific book only.
|
||||
const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern));
|
||||
|
||||
return Response::build_404(*this, request, bookName, reader->getTitle(), details);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
122
src/server/internalServer.h
Normal file
122
src/server/internalServer.h
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2019 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef KIWIXLIB_SERVER_INTERNALSERVER_H
|
||||
#define KIWIXLIB_SERVER_INTERNALSERVER_H
|
||||
|
||||
|
||||
extern "C" {
|
||||
#include "microhttpd_wrapper.h"
|
||||
}
|
||||
|
||||
#include "library.h"
|
||||
#include "name_mapper.h"
|
||||
|
||||
#include <mustache.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include "server/request_context.h"
|
||||
#include "server/response.h"
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
typedef kainjow::mustache::data MustacheData;
|
||||
|
||||
class Entry;
|
||||
class OPDSDumper;
|
||||
|
||||
class InternalServer {
|
||||
public:
|
||||
InternalServer(Library* library,
|
||||
NameMapper* nameMapper,
|
||||
std::string addr,
|
||||
int port,
|
||||
std::string root,
|
||||
int nbThreads,
|
||||
bool verbose,
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton,
|
||||
bool blockExternalLinks);
|
||||
virtual ~InternalServer() = default;
|
||||
|
||||
MHD_Result handlerCallback(struct MHD_Connection* connection,
|
||||
const char* url,
|
||||
const char* method,
|
||||
const char* version,
|
||||
const char* upload_data,
|
||||
size_t* upload_data_size,
|
||||
void** cont_cls);
|
||||
bool start();
|
||||
void stop();
|
||||
|
||||
private: // functions
|
||||
std::unique_ptr<Response> handle_request(const RequestContext& request);
|
||||
std::unique_ptr<Response> build_redirect(const std::string& bookName, const kiwix::Entry& entry) const;
|
||||
std::unique_ptr<Response> build_homepage(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_skin(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2_root(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2_entries(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2_categories(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_meta(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_search(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_suggest(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_random(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_captured_external(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_content(const RequestContext& request);
|
||||
|
||||
std::vector<std::string> search_catalog(const RequestContext& request,
|
||||
kiwix::OPDSDumper& opdsDumper);
|
||||
|
||||
MustacheData get_default_data() const;
|
||||
|
||||
std::shared_ptr<Reader> get_reader(const std::string& bookName) const;
|
||||
bool etag_not_needed(const RequestContext& r) const;
|
||||
ETag get_matching_if_none_match_etag(const RequestContext& request) const;
|
||||
|
||||
private: // data
|
||||
std::string m_addr;
|
||||
int m_port;
|
||||
std::string m_root;
|
||||
int m_nbThreads;
|
||||
std::atomic_bool m_verbose;
|
||||
bool m_withTaskbar;
|
||||
bool m_withLibraryButton;
|
||||
bool m_blockExternalLinks;
|
||||
struct MHD_Daemon* mp_daemon;
|
||||
|
||||
Library* mp_library;
|
||||
NameMapper* mp_nameMapper;
|
||||
|
||||
std::string m_server_id;
|
||||
std::string m_library_id;
|
||||
|
||||
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
|
||||
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage);
|
||||
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
|
||||
friend std::unique_ptr<Response> Response::build_500(const InternalServer& server, const std::string& msg);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif //KIWIXLIB_SERVER_INTERNALSERVER_H
|
||||
109
src/server/internalServer_catalog_v2.cpp
Normal file
109
src/server/internalServer_catalog_v2.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2021 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "internalServer.h"
|
||||
|
||||
#include "library.h"
|
||||
#include "opds_dumper.h"
|
||||
#include "request_context.h"
|
||||
#include "response.h"
|
||||
#include "tools/otherTools.h"
|
||||
#include "kiwixlib-resources.h"
|
||||
|
||||
#include <mustache.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_catalog_v2");
|
||||
}
|
||||
|
||||
std::string url;
|
||||
try {
|
||||
url = request.get_url_part(2);
|
||||
} catch (const std::out_of_range&) {
|
||||
return Response::build_404(*this, request, "", "");
|
||||
}
|
||||
|
||||
if (url == "root.xml") {
|
||||
return handle_catalog_v2_root(request);
|
||||
} else if (url == "searchdescription.xml") {
|
||||
const std::string endpoint_root = m_root + "/catalog/v2";
|
||||
return ContentResponse::build(*this,
|
||||
RESOURCE::catalog_v2_searchdescription_xml,
|
||||
kainjow::mustache::object({{"endpoint_root", endpoint_root}}),
|
||||
"application/opensearchdescription+xml"
|
||||
);
|
||||
} else if (url == "entries") {
|
||||
return handle_catalog_v2_entries(request);
|
||||
} else if (url == "categories") {
|
||||
return handle_catalog_v2_categories(request);
|
||||
} else {
|
||||
return Response::build_404(*this, request, "", "");
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestContext& request)
|
||||
{
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
RESOURCE::templates::catalog_v2_root_xml,
|
||||
kainjow::mustache::object{
|
||||
{"date", gen_date_str()},
|
||||
{"endpoint_root", m_root + "/catalog/v2"},
|
||||
{"feed_id", gen_uuid(m_library_id)},
|
||||
{"all_entries_feed_id", gen_uuid(m_library_id + "/entries")},
|
||||
{"category_list_feed_id", gen_uuid(m_library_id + "/categories")}
|
||||
},
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(m_library_id);
|
||||
const auto bookIds = search_catalog(request, opdsDumper);
|
||||
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query());
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsFeed,
|
||||
"application/atom+xml;profile=opds-catalog;kind=acquisition"
|
||||
);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(m_library_id);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.categoriesOPDSFeed(mp_library->getBooksCategories()),
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
@@ -26,106 +26,84 @@
|
||||
#include <cstdio>
|
||||
#include <atomic>
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
static std::atomic_ullong s_requestIndex(0);
|
||||
|
||||
namespace {
|
||||
|
||||
RequestMethod str2RequestMethod(const std::string& method) {
|
||||
if (method == "GET") return RequestMethod::GET;
|
||||
else if (method == "HEAD") return RequestMethod::HEAD;
|
||||
else if (method == "POST") return RequestMethod::POST;
|
||||
else if (method == "PUT") return RequestMethod::PUT;
|
||||
else if (method == "DELETE") return RequestMethod::DELETE_;
|
||||
else if (method == "CONNECT") return RequestMethod::CONNECT;
|
||||
else if (method == "OPTIONS") return RequestMethod::OPTIONS;
|
||||
else if (method == "TRACE") return RequestMethod::TRACE;
|
||||
else if (method == "PATCH") return RequestMethod::PATCH;
|
||||
else return RequestMethod::OTHER;
|
||||
}
|
||||
|
||||
std::string
|
||||
fullURL2LocalURL(const std::string& full_url, const std::string& rootLocation)
|
||||
{
|
||||
if (rootLocation.empty()) {
|
||||
// nothing special to handle.
|
||||
return full_url;
|
||||
} else if (full_url == rootLocation) {
|
||||
return "/";
|
||||
} else if (full_url.size() > rootLocation.size() &&
|
||||
full_url.substr(0, rootLocation.size()+1) == rootLocation + "/") {
|
||||
return full_url.substr(rootLocation.size());
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
std::string rootLocation,
|
||||
const std::string& _url,
|
||||
const std::string& method,
|
||||
const std::string& _method,
|
||||
const std::string& version) :
|
||||
full_url(_url),
|
||||
url(_url),
|
||||
valid_url(true),
|
||||
url(fullURL2LocalURL(_url, rootLocation)),
|
||||
method(str2RequestMethod(_method)),
|
||||
version(version),
|
||||
requestIndex(s_requestIndex++),
|
||||
acceptEncodingDeflate(false),
|
||||
accept_range(false),
|
||||
range_pair(0, -1)
|
||||
byteRange_()
|
||||
{
|
||||
if (method == "GET") {
|
||||
this->method = RequestMethod::GET;
|
||||
} else if (method == "HEAD") {
|
||||
this->method = RequestMethod::HEAD;
|
||||
} else if (method == "POST") {
|
||||
this->method = RequestMethod::POST;
|
||||
} else if (method == "PUT") {
|
||||
this->method = RequestMethod::PUT;
|
||||
} else if (method == "DELETE") {
|
||||
this->method = RequestMethod::DELETE_;
|
||||
} else if (method == "CONNECT") {
|
||||
this->method = RequestMethod::CONNECT;
|
||||
} else if (method == "OPTIONS") {
|
||||
this->method = RequestMethod::OPTIONS;
|
||||
} else if (method == "TRACE") {
|
||||
this->method = RequestMethod::TRACE;
|
||||
} else if (method == "PATCH") {
|
||||
this->method = RequestMethod::PATCH;
|
||||
} else {
|
||||
this->method = RequestMethod::OTHER;
|
||||
}
|
||||
|
||||
MHD_get_connection_values(connection, MHD_HEADER_KIND, &RequestContext::fill_header, this);
|
||||
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &RequestContext::fill_argument, this);
|
||||
|
||||
valid_url = true;
|
||||
if (rootLocation.empty()) {
|
||||
// nothing special to handle.
|
||||
url = full_url;
|
||||
} else {
|
||||
if (full_url == rootLocation) {
|
||||
url = "/";
|
||||
} else if (full_url.size() > rootLocation.size() &&
|
||||
full_url.substr(0, rootLocation.size()+1) == rootLocation + "/") {
|
||||
url = full_url.substr(rootLocation.size());
|
||||
} else {
|
||||
valid_url = false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
acceptEncodingDeflate =
|
||||
(get_header(MHD_HTTP_HEADER_ACCEPT_ENCODING).find("deflate") != std::string::npos);
|
||||
} catch (const std::out_of_range&) {}
|
||||
|
||||
/*Check if range is requested. */
|
||||
try {
|
||||
auto range = get_header(MHD_HTTP_HEADER_RANGE);
|
||||
int start = 0;
|
||||
int end = -1;
|
||||
std::istringstream iss(range);
|
||||
char c;
|
||||
|
||||
iss >> start >> c;
|
||||
if (iss.good() && c=='-') {
|
||||
iss >> end;
|
||||
if (iss.fail()) {
|
||||
// Something went wrong will extracting.
|
||||
end = -1;
|
||||
}
|
||||
if (iss.eof()) {
|
||||
accept_range = true;
|
||||
range_pair = std::pair<int, int>(start, end);
|
||||
}
|
||||
}
|
||||
byteRange_ = ByteRange::parse(get_header(MHD_HTTP_HEADER_RANGE));
|
||||
} catch (const std::out_of_range&) {}
|
||||
}
|
||||
|
||||
RequestContext::~RequestContext()
|
||||
{}
|
||||
|
||||
|
||||
int RequestContext::fill_header(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char *value)
|
||||
MHD_Result RequestContext::fill_header(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char *value)
|
||||
{
|
||||
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||
_this->headers[key] = value;
|
||||
_this->headers[lcAll(key)] = value;
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
int RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char* value)
|
||||
MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char* value)
|
||||
{
|
||||
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||
_this->arguments[key] = value == nullptr ? "" : value;
|
||||
@@ -147,10 +125,11 @@ void RequestContext::print_debug_info() const {
|
||||
printf(" - %s : '%s'\n", it->first.c_str(), it->second.c_str());
|
||||
}
|
||||
printf("Parsed : \n");
|
||||
printf("full_url: %s\n", full_url.c_str());
|
||||
printf("url : %s\n", url.c_str());
|
||||
printf("acceptEncodingDeflate : %d\n", acceptEncodingDeflate);
|
||||
printf("has_range : %d\n", accept_range);
|
||||
printf("is_valid_url : %d\n", valid_url);
|
||||
printf("has_range : %d\n", byteRange_.kind() != ByteRange::NONE);
|
||||
printf("is_valid_url : %d\n", is_valid_url());
|
||||
printf(".............\n");
|
||||
}
|
||||
|
||||
@@ -188,15 +167,11 @@ std::string RequestContext::get_full_url() const {
|
||||
}
|
||||
|
||||
bool RequestContext::is_valid_url() const {
|
||||
return valid_url;
|
||||
return !url.empty();
|
||||
}
|
||||
|
||||
bool RequestContext::has_range() const {
|
||||
return accept_range;
|
||||
}
|
||||
|
||||
std::pair<int, int> RequestContext::get_range() const {
|
||||
return range_pair;
|
||||
ByteRange RequestContext::get_range() const {
|
||||
return byteRange_;
|
||||
}
|
||||
|
||||
template<>
|
||||
@@ -205,7 +180,17 @@ std::string RequestContext::get_argument(const std::string& name) const {
|
||||
}
|
||||
|
||||
std::string RequestContext::get_header(const std::string& name) const {
|
||||
return headers.at(name);
|
||||
return headers.at(lcAll(name));
|
||||
}
|
||||
|
||||
std::string RequestContext::get_query() const {
|
||||
std::string q;
|
||||
const char* sep = "";
|
||||
for ( const auto& a : arguments ) {
|
||||
q += sep + a.first + '=' + a.second;
|
||||
sep = "&";
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,8 +27,10 @@
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "byte_range.h"
|
||||
|
||||
extern "C" {
|
||||
#include <microhttpd.h>
|
||||
#include "microhttpd_wrapper.h"
|
||||
}
|
||||
|
||||
namespace kiwix {
|
||||
@@ -51,7 +53,7 @@ class IndexError: public std::runtime_error {};
|
||||
|
||||
|
||||
class RequestContext {
|
||||
public:
|
||||
public: // functions
|
||||
RequestContext(struct MHD_Connection* connection,
|
||||
std::string rootLocation,
|
||||
const std::string& url,
|
||||
@@ -72,34 +74,42 @@ class RequestContext {
|
||||
return v;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
T get_optional_param(const std::string& name, T default_value) const
|
||||
{
|
||||
try {
|
||||
return get_argument<T>(name);
|
||||
} catch (...) {}
|
||||
return default_value;
|
||||
}
|
||||
|
||||
|
||||
RequestMethod get_method() const;
|
||||
std::string get_url() const;
|
||||
std::string get_url_part(int part) const;
|
||||
std::string get_full_url() const;
|
||||
std::string get_query() const;
|
||||
|
||||
bool has_range() const;
|
||||
std::pair<int, int> get_range() const;
|
||||
ByteRange get_range() const;
|
||||
|
||||
bool can_compress() const { return acceptEncodingDeflate; }
|
||||
|
||||
private:
|
||||
private: // data
|
||||
std::string full_url;
|
||||
std::string url;
|
||||
bool valid_url;
|
||||
RequestMethod method;
|
||||
std::string version;
|
||||
unsigned long long requestIndex;
|
||||
|
||||
bool acceptEncodingDeflate;
|
||||
|
||||
bool accept_range;
|
||||
std::pair<int, int> range_pair;
|
||||
ByteRange byteRange_;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::map<std::string, std::string> arguments;
|
||||
|
||||
static int fill_header(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
static int fill_argument(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
private: // functions
|
||||
static MHD_Result fill_header(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
static MHD_Result fill_argument(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
};
|
||||
|
||||
template<> std::string RequestContext::get_argument(const std::string& name) const;
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
|
||||
|
||||
/*
|
||||
* Copyright 2019 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "response.h"
|
||||
#include "request_context.h"
|
||||
#include "internalServer.h"
|
||||
#include "kiwixlib-resources.h"
|
||||
|
||||
#include "tools/regexTools.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include "tools/otherTools.h"
|
||||
|
||||
#include "string.h"
|
||||
#include <mustache.hpp>
|
||||
@@ -17,25 +35,102 @@
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
Response::Response(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton)
|
||||
: m_verbose(verbose),
|
||||
m_root(root),
|
||||
m_content(""),
|
||||
m_mimeType(""),
|
||||
m_returnCode(MHD_HTTP_OK),
|
||||
m_withTaskbar(withTaskbar),
|
||||
m_withLibraryButton(withLibraryButton),
|
||||
m_useCache(false),
|
||||
m_addTaskbar(false),
|
||||
m_bookName(""),
|
||||
m_startRange(0),
|
||||
m_lenRange(0)
|
||||
namespace
|
||||
{
|
||||
// some utilities
|
||||
|
||||
std::string get_mime_type(const zim::Item& item)
|
||||
{
|
||||
try {
|
||||
return item.getMimetype();
|
||||
} catch (exception& e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
bool is_compressible_mime_type(const std::string& mimeType)
|
||||
{
|
||||
return mimeType.find("text/") != string::npos
|
||||
|| mimeType.find("application/javascript") != string::npos
|
||||
|| mimeType.find("application/atom") != string::npos
|
||||
|| mimeType.find("application/opensearchdescription") != string::npos
|
||||
|| mimeType.find("application/json") != string::npos;
|
||||
}
|
||||
|
||||
|
||||
static int print_key_value (void *cls, enum MHD_ValueKind kind,
|
||||
const char *key, const char *value)
|
||||
} // unnamed namespace
|
||||
|
||||
Response::Response(bool verbose)
|
||||
: m_verbose(verbose),
|
||||
m_returnCode(MHD_HTTP_OK)
|
||||
{
|
||||
add_header(MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build(const InternalServer& server)
|
||||
{
|
||||
return std::unique_ptr<Response>(new Response(server.m_verbose.load()));
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_304(const InternalServer& server, const ETag& etag)
|
||||
{
|
||||
auto response = Response::build(server);
|
||||
response->set_code(MHD_HTTP_NOT_MODIFIED);
|
||||
response->m_etag = etag;
|
||||
if ( etag.get_option(ETag::COMPRESSED_CONTENT) ) {
|
||||
response->add_header(MHD_HTTP_HEADER_VARY, "Accept-Encoding");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_404(const InternalServer& server, const RequestContext& request, const std::string& bookName, const std::string& bookTitle, const std::string& details)
|
||||
{
|
||||
MustacheData results;
|
||||
results.set("url", request.get_full_url());
|
||||
results.set("details", details);
|
||||
|
||||
auto response = ContentResponse::build(server, RESOURCE::templates::_404_html, results, "text/html");
|
||||
response->set_code(MHD_HTTP_NOT_FOUND);
|
||||
response->set_taskbar(bookName, bookTitle);
|
||||
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
|
||||
{
|
||||
auto response = Response::build(server);
|
||||
// [FIXME] (compile with recent enough version of libmicrohttpd)
|
||||
// response->set_code(MHD_HTTP_RANGE_NOT_SATISFIABLE);
|
||||
response->set_code(416);
|
||||
std::ostringstream oss;
|
||||
oss << "bytes */" << resourceLength;
|
||||
response->add_header(MHD_HTTP_HEADER_CONTENT_RANGE, oss.str());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_500(const InternalServer& server, const std::string& msg)
|
||||
{
|
||||
MustacheData data;
|
||||
data.set("error", msg);
|
||||
auto content = render_template(RESOURCE::templates::_500_html, data);
|
||||
std::unique_ptr<Response> response (
|
||||
new ContentResponse(server.m_root, true, false, false, false, content, "text/html"));
|
||||
response->set_code(MHD_HTTP_INTERNAL_SERVER_ERROR);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<Response> Response::build_redirect(const InternalServer& server, const std::string& redirectUrl)
|
||||
{
|
||||
auto response = Response::build(server);
|
||||
response->m_returnCode = MHD_HTTP_FOUND;
|
||||
response->add_header(MHD_HTTP_HEADER_LOCATION, redirectUrl);
|
||||
return response;
|
||||
}
|
||||
|
||||
static MHD_Result print_key_value (void *cls, enum MHD_ValueKind kind,
|
||||
const char *key, const char *value)
|
||||
{
|
||||
printf (" - %s: '%s'\n", key, value);
|
||||
return MHD_YES;
|
||||
@@ -43,17 +138,17 @@ static int print_key_value (void *cls, enum MHD_ValueKind kind,
|
||||
|
||||
|
||||
struct RunningResponse {
|
||||
kiwix::Entry entry;
|
||||
zim::Item item;
|
||||
int range_start;
|
||||
|
||||
RunningResponse(kiwix::Entry entry,
|
||||
RunningResponse(zim::Item item,
|
||||
int range_start) :
|
||||
entry(entry),
|
||||
item(item),
|
||||
range_start(range_start)
|
||||
{}
|
||||
};
|
||||
|
||||
static ssize_t callback_reader_from_entry(void* cls,
|
||||
static ssize_t callback_reader_from_item(void* cls,
|
||||
uint64_t pos,
|
||||
char* buf,
|
||||
size_t max)
|
||||
@@ -62,13 +157,13 @@ static ssize_t callback_reader_from_entry(void* cls,
|
||||
|
||||
size_t max_size_to_set = min<size_t>(
|
||||
max,
|
||||
response->entry.getSize() - pos - response->range_start);
|
||||
response->item.getSize() - pos - response->range_start);
|
||||
|
||||
if (max_size_to_set <= 0) {
|
||||
return MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
|
||||
zim::Blob blob = response->entry.getBlob(response->range_start+pos, max_size_to_set);
|
||||
zim::Blob blob = response->item.getData(response->range_start+pos, max_size_to_set);
|
||||
memcpy(buf, blob.data(), max_size_to_set);
|
||||
return max_size_to_set;
|
||||
}
|
||||
@@ -90,33 +185,18 @@ void print_response_info(int retCode, MHD_Response* response)
|
||||
}
|
||||
|
||||
|
||||
std::string render_template(const std::string& template_str, kainjow::mustache::data data)
|
||||
void ContentResponse::introduce_taskbar()
|
||||
{
|
||||
kainjow::mustache::mustache tmpl(template_str);
|
||||
kainjow::mustache::data urlencode{kainjow::mustache::lambda2{
|
||||
[](const std::string& str,const kainjow::mustache::renderer& r) { return urlEncode(r(str), true); }}};
|
||||
data.set("urlencoded", urlencode);
|
||||
std::stringstream ss;
|
||||
tmpl.render(data, [&ss](const std::string& str) { ss << str; });
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
|
||||
void Response::introduce_taskbar()
|
||||
{
|
||||
if (! m_withTaskbar)
|
||||
// Taskbar is globally disabled.
|
||||
return;
|
||||
kainjow::mustache::data data;
|
||||
data.set("root", m_root);
|
||||
data.set("content", m_bookName);
|
||||
data.set("hascontent", !m_bookName.empty());
|
||||
data.set("hascontent", (!m_bookName.empty() && !m_bookTitle.empty()));
|
||||
data.set("title", m_bookTitle);
|
||||
data.set("withlibrarybutton", m_withLibraryButton);
|
||||
auto head_content = render_template(RESOURCE::templates::head_part_html, data);
|
||||
m_content = appendToFirstOccurence(
|
||||
auto head_content = render_template(RESOURCE::templates::head_taskbar_html, data);
|
||||
m_content = prependToFirstOccurence(
|
||||
m_content,
|
||||
"<head>",
|
||||
"</head[ \\t]*>",
|
||||
head_content);
|
||||
|
||||
auto taskbar_part = render_template(RESOURCE::templates::taskbar_part_html, data);
|
||||
@@ -127,88 +207,107 @@ void Response::introduce_taskbar()
|
||||
}
|
||||
|
||||
|
||||
|
||||
int Response::send(const RequestContext& request, MHD_Connection* connection)
|
||||
void ContentResponse::inject_externallinks_blocker()
|
||||
{
|
||||
MHD_Response* response = nullptr;
|
||||
switch (m_mode) {
|
||||
case ResponseMode::RAW_CONTENT : {
|
||||
if (m_addTaskbar) {
|
||||
introduce_taskbar();
|
||||
}
|
||||
kainjow::mustache::data data;
|
||||
data.set("root", m_root);
|
||||
auto script_tag = render_template(RESOURCE::templates::external_blocker_part_html, data);
|
||||
m_content = prependToFirstOccurence(
|
||||
m_content,
|
||||
"</head[ \\t]*>",
|
||||
script_tag);
|
||||
}
|
||||
|
||||
bool shouldCompress = m_compress && request.can_compress();
|
||||
shouldCompress &= m_mimeType.find("text/") != string::npos
|
||||
|| m_mimeType.find("application/javascript") != string::npos
|
||||
|| m_mimeType.find("application/json") != string::npos;
|
||||
void ContentResponse::inject_root_link(){
|
||||
m_content = prependToFirstOccurence(
|
||||
m_content,
|
||||
"</head[ \\t]*>",
|
||||
"<link type=\"root\" href=\"" + m_root + "\">");
|
||||
}
|
||||
|
||||
shouldCompress &= (m_content.size() > KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE);
|
||||
bool
|
||||
ContentResponse::can_compress(const RequestContext& request) const
|
||||
{
|
||||
return request.can_compress()
|
||||
&& is_compressible_mime_type(m_mimeType)
|
||||
&& (m_content.size() > KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE);
|
||||
}
|
||||
|
||||
if (shouldCompress) {
|
||||
std::vector<Bytef> compr_buffer(compressBound(m_content.size()));
|
||||
uLongf comprLen = compr_buffer.capacity();
|
||||
int err = compress(&compr_buffer[0],
|
||||
&comprLen,
|
||||
(const Bytef*)(m_content.data()),
|
||||
m_content.size());
|
||||
if (err == Z_OK && comprLen > 2 && comprLen < (m_content.size() + 2)) {
|
||||
/* /!\ Internet Explorer has a bug with deflate compression.
|
||||
It can not handle the first two bytes (compression headers)
|
||||
We need to chunk them off (move the content 2bytes)
|
||||
It has no incidence on other browsers
|
||||
See http://www.subbu.org/blog/2008/03/ie7-deflate-or-not and comments */
|
||||
m_content = string((char*)&compr_buffer[2], comprLen - 2);
|
||||
} else {
|
||||
shouldCompress = false;
|
||||
}
|
||||
}
|
||||
bool
|
||||
ContentResponse::contentDecorationAllowed() const
|
||||
{
|
||||
return (startsWith(m_mimeType, "text/html")
|
||||
&& m_mimeType.find(";raw=true") == std::string::npos);
|
||||
}
|
||||
|
||||
response = MHD_create_response_from_buffer(
|
||||
m_content.size(), const_cast<char*>(m_content.data()), MHD_RESPMEM_MUST_COPY);
|
||||
MHD_Response*
|
||||
Response::create_mhd_response(const RequestContext& request)
|
||||
{
|
||||
MHD_Response* response = MHD_create_response_from_buffer(0, nullptr, MHD_RESPMEM_PERSISTENT);
|
||||
return response;
|
||||
}
|
||||
|
||||
if (shouldCompress) {
|
||||
MHD_add_response_header(
|
||||
response, MHD_HTTP_HEADER_VARY, "Accept-Encoding");
|
||||
MHD_add_response_header(
|
||||
response, MHD_HTTP_HEADER_CONTENT_ENCODING, "deflate");
|
||||
}
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType.c_str());
|
||||
break;
|
||||
MHD_Response*
|
||||
ContentResponse::create_mhd_response(const RequestContext& request)
|
||||
{
|
||||
if (contentDecorationAllowed()) {
|
||||
inject_root_link();
|
||||
|
||||
if (m_withTaskbar) {
|
||||
introduce_taskbar();
|
||||
}
|
||||
|
||||
case ResponseMode::REDIRECTION : {
|
||||
response = MHD_create_response_from_buffer(0, nullptr, MHD_RESPMEM_MUST_COPY);
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_LOCATION, m_content.c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
case ResponseMode::ENTRY : {
|
||||
response = MHD_create_response_from_callback(m_entry.getSize(),
|
||||
16384,
|
||||
callback_reader_from_entry,
|
||||
new RunningResponse(m_entry, m_startRange),
|
||||
callback_free_response);
|
||||
MHD_add_response_header(response,
|
||||
MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType.c_str());
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_ACCEPT_RANGES, "bytes");
|
||||
std::ostringstream oss;
|
||||
oss << "bytes " << m_startRange << "-" << m_startRange + m_lenRange - 1
|
||||
<< "/" << m_entry.getSize();
|
||||
|
||||
MHD_add_response_header(response,
|
||||
MHD_HTTP_HEADER_CONTENT_RANGE, oss.str().c_str());
|
||||
|
||||
MHD_add_response_header(response,
|
||||
MHD_HTTP_HEADER_CONTENT_LENGTH, kiwix::to_string(m_lenRange).c_str());
|
||||
break;
|
||||
if (m_blockExternalLinks) {
|
||||
inject_externallinks_blocker();
|
||||
}
|
||||
}
|
||||
|
||||
MHD_add_response_header(response, "Access-Control-Allow-Origin", "*");
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
|
||||
m_useCache ? "max-age=2723040, public" : "no-cache, no-store, must-revalidate");
|
||||
bool shouldCompress = can_compress(request);
|
||||
if (shouldCompress) {
|
||||
std::vector<Bytef> compr_buffer(compressBound(m_content.size()));
|
||||
uLongf comprLen = compr_buffer.capacity();
|
||||
int err = compress(&compr_buffer[0],
|
||||
&comprLen,
|
||||
(const Bytef*)(m_content.data()),
|
||||
m_content.size());
|
||||
if (err == Z_OK && comprLen > 2 && comprLen < (m_content.size() + 2)) {
|
||||
/* /!\ Internet Explorer has a bug with deflate compression.
|
||||
It can not handle the first two bytes (compression headers)
|
||||
We need to chunk them off (move the content 2bytes)
|
||||
It has no incidence on other browsers
|
||||
See http://www.subbu.org/blog/2008/03/ie7-deflate-or-not and comments */
|
||||
m_content = string((char*)&compr_buffer[2], comprLen - 2);
|
||||
m_etag.set_option(ETag::COMPRESSED_CONTENT);
|
||||
} else {
|
||||
shouldCompress = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_returnCode == MHD_HTTP_OK && request.has_range())
|
||||
MHD_Response* response = MHD_create_response_from_buffer(
|
||||
m_content.size(), const_cast<char*>(m_content.data()), MHD_RESPMEM_MUST_COPY);
|
||||
|
||||
if (shouldCompress) {
|
||||
MHD_add_response_header(
|
||||
response, MHD_HTTP_HEADER_VARY, "Accept-Encoding");
|
||||
MHD_add_response_header(
|
||||
response, MHD_HTTP_HEADER_CONTENT_ENCODING, "deflate");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
MHD_Result Response::send(const RequestContext& request, MHD_Connection* connection)
|
||||
{
|
||||
MHD_Response* response = create_mhd_response(request);
|
||||
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
|
||||
m_etag.get_option(ETag::CACHEABLE_ENTITY) ? "max-age=2723040, public" : "no-cache, no-store, must-revalidate");
|
||||
const std::string etag = m_etag.get_etag();
|
||||
if ( ! etag.empty() )
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_ETAG, etag.c_str());
|
||||
for(auto& p: m_customHeaders) {
|
||||
MHD_add_response_header(response, p.first.c_str(), p.second.c_str());
|
||||
}
|
||||
|
||||
if (m_returnCode == MHD_HTTP_OK && m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT)
|
||||
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
|
||||
|
||||
if (m_verbose)
|
||||
@@ -219,32 +318,103 @@ int Response::send(const RequestContext& request, MHD_Connection* connection)
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Response::set_template(const std::string& template_str, kainjow::mustache::data data) {
|
||||
set_content(render_template(template_str, data));
|
||||
}
|
||||
|
||||
void Response::set_content(const std::string& content) {
|
||||
m_content = content;
|
||||
m_mode = ResponseMode::RAW_CONTENT;
|
||||
}
|
||||
|
||||
void Response::set_redirection(const std::string& url) {
|
||||
m_content = url;
|
||||
m_mode = ResponseMode::REDIRECTION;
|
||||
m_returnCode = MHD_HTTP_FOUND;
|
||||
}
|
||||
|
||||
void Response::set_entry(const Entry& entry) {
|
||||
m_entry = entry;
|
||||
m_mode = ResponseMode::ENTRY;
|
||||
}
|
||||
|
||||
void Response::set_taskbar(const std::string& bookName, const std::string& bookTitle)
|
||||
void ContentResponse::set_taskbar(const std::string& bookName, const std::string& bookTitle)
|
||||
{
|
||||
m_addTaskbar = true;
|
||||
m_bookName = bookName;
|
||||
m_bookTitle = bookTitle;
|
||||
}
|
||||
|
||||
|
||||
ContentResponse::ContentResponse(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks, const std::string& content, const std::string& mimetype) :
|
||||
Response(verbose),
|
||||
m_root(root),
|
||||
m_content(content),
|
||||
m_mimeType(mimetype),
|
||||
m_withTaskbar(withTaskbar),
|
||||
m_withLibraryButton(withLibraryButton),
|
||||
m_blockExternalLinks(blockExternalLinks),
|
||||
m_bookName(""),
|
||||
m_bookTitle("")
|
||||
{
|
||||
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
|
||||
}
|
||||
|
||||
std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage)
|
||||
{
|
||||
return std::unique_ptr<ContentResponse>(new ContentResponse(
|
||||
server.m_root,
|
||||
server.m_verbose.load(),
|
||||
server.m_withTaskbar && !isHomePage,
|
||||
server.m_withLibraryButton,
|
||||
server.m_blockExternalLinks,
|
||||
content,
|
||||
mimetype));
|
||||
}
|
||||
|
||||
std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype, bool isHomePage) {
|
||||
auto content = render_template(template_str, data);
|
||||
return ContentResponse::build(server, content, mimetype, isHomePage);
|
||||
}
|
||||
|
||||
ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
|
||||
Response(verbose),
|
||||
m_item(item),
|
||||
m_mimeType(mimetype)
|
||||
{
|
||||
m_byteRange = byterange;
|
||||
set_cacheable();
|
||||
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item)
|
||||
{
|
||||
const std::string mimetype = get_mime_type(item);
|
||||
auto byteRange = request.get_range().resolve(item.getSize());
|
||||
const bool noRange = byteRange.kind() == ByteRange::RESOLVED_FULL_CONTENT;
|
||||
if (noRange && is_compressible_mime_type(mimetype)) {
|
||||
// Return a contentResponse
|
||||
auto response = ContentResponse::build(server, item.getData(), mimetype);
|
||||
response->set_cacheable();
|
||||
response->m_byteRange = byteRange;
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
if (byteRange.kind() == ByteRange::RESOLVED_UNSATISFIABLE) {
|
||||
auto response = Response::build_416(server, item.getSize());
|
||||
response->set_cacheable();
|
||||
return response;
|
||||
}
|
||||
|
||||
return std::unique_ptr<Response>(new ItemResponse(
|
||||
server.m_verbose.load(),
|
||||
item,
|
||||
mimetype,
|
||||
byteRange));
|
||||
}
|
||||
|
||||
MHD_Response*
|
||||
ItemResponse::create_mhd_response(const RequestContext& request)
|
||||
{
|
||||
const auto content_length = m_byteRange.length();
|
||||
MHD_Response* response = MHD_create_response_from_callback(content_length,
|
||||
16384,
|
||||
callback_reader_from_item,
|
||||
new RunningResponse(m_item, m_byteRange.first()),
|
||||
callback_free_response);
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_ACCEPT_RANGES, "bytes");
|
||||
if ( m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT ) {
|
||||
std::ostringstream oss;
|
||||
oss << "bytes " << m_byteRange.first() << "-" << m_byteRange.last()
|
||||
<< "/" << m_item.getSize();
|
||||
|
||||
MHD_add_response_header(response,
|
||||
MHD_HTTP_HEADER_CONTENT_RANGE, oss.str().c_str());
|
||||
}
|
||||
|
||||
MHD_add_response_header(response,
|
||||
MHD_HTTP_HEADER_CONTENT_LENGTH, kiwix::to_string(content_length).c_str());
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -22,66 +22,99 @@
|
||||
#define KIWIXLIB_SERVER_RESPONSE_H
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
#include <mustache.hpp>
|
||||
#include "byte_range.h"
|
||||
#include "entry.h"
|
||||
#include "etag.h"
|
||||
|
||||
extern "C" {
|
||||
#include <microhttpd.h>
|
||||
#include "microhttpd_wrapper.h"
|
||||
}
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
enum class ResponseMode {
|
||||
RAW_CONTENT,
|
||||
REDIRECTION,
|
||||
ENTRY
|
||||
};
|
||||
|
||||
class InternalServer;
|
||||
class RequestContext;
|
||||
|
||||
class EntryResponse;
|
||||
|
||||
class Response {
|
||||
public:
|
||||
Response(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton);
|
||||
~Response() = default;
|
||||
Response(bool verbose);
|
||||
virtual ~Response() = default;
|
||||
|
||||
int send(const RequestContext& request, MHD_Connection* connection);
|
||||
static std::unique_ptr<Response> build(const InternalServer& server);
|
||||
static std::unique_ptr<Response> build_304(const InternalServer& server, const ETag& etag);
|
||||
static std::unique_ptr<Response> build_404(const InternalServer& server, const RequestContext& request, const std::string& bookName, const std::string& bookTitle, const std::string& details="");
|
||||
static std::unique_ptr<Response> build_416(const InternalServer& server, size_t resourceLength);
|
||||
static std::unique_ptr<Response> build_500(const InternalServer& server, const std::string& msg);
|
||||
static std::unique_ptr<Response> build_redirect(const InternalServer& server, const std::string& redirectUrl);
|
||||
|
||||
void set_template(const std::string& template_str, kainjow::mustache::data data);
|
||||
void set_content(const std::string& content);
|
||||
void set_redirection(const std::string& url);
|
||||
void set_entry(const Entry& entry);
|
||||
MHD_Result send(const RequestContext& request, MHD_Connection* connection);
|
||||
|
||||
|
||||
void set_mimeType(const std::string& mimeType) { m_mimeType = mimeType; }
|
||||
void set_code(int code) { m_returnCode = code; }
|
||||
void set_cache(bool cache) { m_useCache = cache; }
|
||||
void set_compress(bool compress) { m_compress = compress; }
|
||||
void set_cacheable() { m_etag.set_option(ETag::CACHEABLE_ENTITY); }
|
||||
void set_server_id(const std::string& id) { m_etag.set_server_id(id); }
|
||||
void add_header(const std::string& name, const std::string& value) { m_customHeaders[name] = value; }
|
||||
|
||||
int getReturnCode() const { return m_returnCode; }
|
||||
|
||||
private: // functions
|
||||
virtual MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
MHD_Response* create_error_response(const RequestContext& request) const;
|
||||
|
||||
protected: // data
|
||||
bool m_verbose;
|
||||
int m_returnCode;
|
||||
ByteRange m_byteRange;
|
||||
ETag m_etag;
|
||||
std::map<std::string, std::string> m_customHeaders;
|
||||
|
||||
friend class ItemResponse;
|
||||
};
|
||||
|
||||
|
||||
class ContentResponse : public Response {
|
||||
public:
|
||||
ContentResponse(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks, const std::string& content, const std::string& mimetype);
|
||||
static std::unique_ptr<ContentResponse> build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage = false);
|
||||
static std::unique_ptr<ContentResponse> build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype, bool isHomePage = false);
|
||||
|
||||
void set_taskbar(const std::string& bookName, const std::string& bookTitle);
|
||||
void set_range_first(uint64_t start) { m_startRange = start; }
|
||||
void set_range_len(uint64_t len) { m_lenRange = len; }
|
||||
|
||||
int getReturnCode() { return m_returnCode; }
|
||||
|
||||
void introduce_taskbar();
|
||||
|
||||
private:
|
||||
bool m_verbose;
|
||||
ResponseMode m_mode;
|
||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
|
||||
void introduce_taskbar();
|
||||
void inject_externallinks_blocker();
|
||||
void inject_root_link();
|
||||
bool can_compress(const RequestContext& request) const;
|
||||
bool contentDecorationAllowed() const;
|
||||
|
||||
|
||||
private:
|
||||
std::string m_root;
|
||||
std::string m_content;
|
||||
Entry m_entry;
|
||||
std::string m_mimeType;
|
||||
int m_returnCode;
|
||||
bool m_withTaskbar;
|
||||
bool m_withLibraryButton;
|
||||
bool m_useCache;
|
||||
bool m_compress;
|
||||
bool m_addTaskbar;
|
||||
bool m_blockExternalLinks;
|
||||
std::string m_bookName;
|
||||
std::string m_bookTitle;
|
||||
uint64_t m_startRange;
|
||||
uint64_t m_lenRange;
|
||||
};
|
||||
|
||||
class ItemResponse : public Response {
|
||||
public:
|
||||
ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
|
||||
static std::unique_ptr<Response> build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
|
||||
|
||||
private:
|
||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
|
||||
zim::Item m_item;
|
||||
std::string m_mimeType;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -12,45 +12,31 @@
|
||||
UnixImpl::UnixImpl():
|
||||
m_pid(0),
|
||||
m_running(false),
|
||||
m_mutex(PTHREAD_MUTEX_INITIALIZER),
|
||||
m_waitingThread()
|
||||
m_shouldQuit(false)
|
||||
{
|
||||
}
|
||||
|
||||
UnixImpl::~UnixImpl()
|
||||
{
|
||||
kill();
|
||||
// Android has no pthread_cancel :(
|
||||
#ifdef __ANDROID__
|
||||
pthread_kill(m_waitingThread, SIGUSR1);
|
||||
#else
|
||||
pthread_cancel(m_waitingThread);
|
||||
#endif
|
||||
m_shouldQuit = true;
|
||||
m_waitingThread.join();
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
void thread_exit_handler(int sig) {
|
||||
pthread_exit(0);
|
||||
}
|
||||
#endif
|
||||
|
||||
void* UnixImpl::waitForPID(void* _self)
|
||||
{
|
||||
#ifdef __ANDROID__
|
||||
struct sigaction actions;
|
||||
memset(&actions, 0, sizeof(actions));
|
||||
sigemptyset(&actions.sa_mask);
|
||||
actions.sa_flags = 0;
|
||||
actions.sa_handler = thread_exit_handler;
|
||||
sigaction(SIGUSR1, &actions, NULL);
|
||||
#endif
|
||||
|
||||
UnixImpl* self = static_cast<UnixImpl*>(_self);
|
||||
waitpid(self->m_pid, NULL, 0);
|
||||
while (true) {
|
||||
if (!waitpid(self->m_pid, NULL, WNOHANG)) {
|
||||
break;
|
||||
}
|
||||
if (self->m_shouldQuit) {
|
||||
return nullptr;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&self->m_mutex);
|
||||
self->m_running = false;
|
||||
pthread_mutex_unlock(&self->m_mutex);
|
||||
|
||||
return self;
|
||||
}
|
||||
@@ -74,7 +60,7 @@ void UnixImpl::run(commandLine_t& commandLine)
|
||||
default:
|
||||
m_pid = pid;
|
||||
m_running = true;
|
||||
pthread_create(&m_waitingThread, NULL, waitForPID, this);
|
||||
m_waitingThread = std::thread(waitForPID, this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -86,8 +72,5 @@ bool UnixImpl::kill()
|
||||
|
||||
bool UnixImpl::isRunning()
|
||||
{
|
||||
pthread_mutex_lock(&m_mutex);
|
||||
bool ret = m_running;
|
||||
pthread_mutex_unlock(&m_mutex);
|
||||
return ret;
|
||||
return m_running;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
|
||||
#include "subprocess.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
class UnixImpl : public SubprocessImpl
|
||||
{
|
||||
private:
|
||||
int m_pid;
|
||||
bool m_running;
|
||||
pthread_mutex_t m_mutex;
|
||||
pthread_t m_waitingThread;
|
||||
std::atomic<bool> m_running;
|
||||
std::atomic<bool> m_shouldQuit;
|
||||
std::thread m_waitingThread;
|
||||
|
||||
public:
|
||||
UnixImpl();
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
#include <windows.h>
|
||||
#include <winbase.h>
|
||||
#include <shlwapi.h>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
WinImpl::WinImpl():
|
||||
m_pid(0),
|
||||
m_running(false),
|
||||
m_handle(INVALID_HANDLE_VALUE)
|
||||
m_subprocessHandle(INVALID_HANDLE_VALUE),
|
||||
m_waitingThreadHandle(INVALID_HANDLE_VALUE)
|
||||
{
|
||||
InitializeCriticalSection(&m_criticalSection);
|
||||
}
|
||||
@@ -18,14 +20,15 @@ WinImpl::WinImpl():
|
||||
WinImpl::~WinImpl()
|
||||
{
|
||||
kill();
|
||||
CloseHandle(m_handle);
|
||||
WaitForSingleObject(m_waitingThreadHandle, INFINITE);
|
||||
CloseHandle(m_subprocessHandle);
|
||||
DeleteCriticalSection(&m_criticalSection);
|
||||
}
|
||||
|
||||
DWORD WINAPI WinImpl::waitForPID(void* _self)
|
||||
{
|
||||
WinImpl* self = static_cast<WinImpl*>(_self);
|
||||
WaitForSingleObject(self->m_handle, INFINITE);
|
||||
WaitForSingleObject(self->m_subprocessHandle, INFINITE);
|
||||
|
||||
EnterCriticalSection(&self->m_criticalSection);
|
||||
self->m_running = false;
|
||||
@@ -34,11 +37,11 @@ DWORD WINAPI WinImpl::waitForPID(void* _self)
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::unique_ptr<wchar_t[]> toWideChar(const std::string& value)
|
||||
std::unique_ptr<wchar_t[]> toWideChar(const std::string& value, size_t min_size = 0)
|
||||
{
|
||||
auto size = MultiByteToWideChar(CP_UTF8, 0,
|
||||
size_t size = MultiByteToWideChar(CP_UTF8, 0,
|
||||
value.c_str(), -1, nullptr, 0);
|
||||
auto wdata = std::unique_ptr<wchar_t[]>(new wchar_t[size]);
|
||||
auto wdata = std::unique_ptr<wchar_t[]>(new wchar_t[size>min_size?size:min_size]);
|
||||
auto ret = MultiByteToWideChar(CP_UTF8, 0,
|
||||
value.c_str(), -1, wdata.get(), size);
|
||||
if (0 == ret) {
|
||||
@@ -46,6 +49,9 @@ std::unique_ptr<wchar_t[]> toWideChar(const std::string& value)
|
||||
oss << "Cannot convert to wchar : " << GetLastError();
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
if (size < min_size) {
|
||||
memset(wdata.get() + size, 0, min_size-size);
|
||||
}
|
||||
return wdata;
|
||||
}
|
||||
|
||||
@@ -55,14 +61,16 @@ void WinImpl::run(commandLine_t& commandLine)
|
||||
STARTUPINFOW startInfo = {0};
|
||||
PROCESS_INFORMATION procInfo;
|
||||
startInfo.cb = sizeof(startInfo);
|
||||
std::ostringstream oss;
|
||||
std::wostringstream oss;
|
||||
for(auto& item: commandLine) {
|
||||
oss << item << " ";
|
||||
auto witem = toWideChar(item, MAX_PATH);
|
||||
PathQuoteSpacesW(witem.get());
|
||||
oss << witem.get() << " ";
|
||||
}
|
||||
auto wCommandLine = toWideChar(oss.str());
|
||||
auto wCommandLine = oss.str();
|
||||
if (CreateProcessW(
|
||||
NULL,
|
||||
wCommandLine.get(),
|
||||
const_cast<wchar_t*>(wCommandLine.c_str()),
|
||||
NULL,
|
||||
NULL,
|
||||
false,
|
||||
@@ -70,18 +78,19 @@ void WinImpl::run(commandLine_t& commandLine)
|
||||
NULL,
|
||||
NULL,
|
||||
&startInfo,
|
||||
&procInfo)) {
|
||||
&procInfo))
|
||||
{
|
||||
m_pid = procInfo.dwProcessId;
|
||||
m_handle = procInfo.hProcess;
|
||||
m_subprocessHandle = procInfo.hProcess;
|
||||
CloseHandle(procInfo.hThread);
|
||||
m_running = true;
|
||||
CreateThread(NULL, 0, &waitForPID, this, 0, NULL );
|
||||
m_waitingThreadHandle = CreateThread(NULL, 0, &waitForPID, this, 0, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
bool WinImpl::kill()
|
||||
{
|
||||
return TerminateProcess(m_handle, 0);
|
||||
return TerminateProcess(m_subprocessHandle, 0);
|
||||
}
|
||||
|
||||
bool WinImpl::isRunning()
|
||||
|
||||
@@ -11,7 +11,8 @@ class WinImpl : public SubprocessImpl
|
||||
private:
|
||||
int m_pid;
|
||||
bool m_running;
|
||||
HANDLE m_handle;
|
||||
HANDLE m_subprocessHandle;
|
||||
HANDLE m_waitingThreadHandle;
|
||||
CRITICAL_SECTION m_criticalSection;
|
||||
|
||||
public:
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
|
||||
#include "tools/otherTools.h"
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -25,9 +27,14 @@
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include <zim/uuid.h>
|
||||
|
||||
|
||||
static std::map<std::string, std::string> codeisomapping {
|
||||
{ "aa", "aar" },
|
||||
@@ -204,3 +211,168 @@ std::string kiwix::nodeToString(const pugi::xml_node& node)
|
||||
std::string kiwix::converta2toa3(const std::string& a2code){
|
||||
return codeisomapping.at(a2code);
|
||||
}
|
||||
|
||||
std::vector<std::string> kiwix::convertTags(const std::string& tags_str)
|
||||
{
|
||||
auto tags = kiwix::split(tags_str, ";");
|
||||
std::vector<std::string> tagsList;
|
||||
bool picSeen(false), vidSeen(false), detSeen(false), indexSeen(false);
|
||||
for (auto tag: tags) {
|
||||
picSeen |= (tag == "nopic" || startsWith(tag, "_pictures:"));
|
||||
vidSeen |= (tag == "novid" || startsWith(tag, "_videos:"));
|
||||
detSeen |= (tag == "nodet" || startsWith(tag, "_details:"));
|
||||
indexSeen |= kiwix::startsWith(tag, "_ftindex");
|
||||
if (tag == "nopic") {
|
||||
tagsList.push_back("_pictures:no");
|
||||
} else if (tag == "novid") {
|
||||
tagsList.push_back("_videos:no");
|
||||
} else if (tag == "nodet") {
|
||||
tagsList.push_back("_details:no");
|
||||
} else if (tag == "_ftindex") {
|
||||
tagsList.push_back("_ftindex:yes");
|
||||
} else {
|
||||
tagsList.push_back(tag);
|
||||
}
|
||||
}
|
||||
if (!indexSeen) {
|
||||
tagsList.push_back("_ftindex:no");
|
||||
}
|
||||
if (!picSeen) {
|
||||
tagsList.push_back("_pictures:yes");
|
||||
}
|
||||
if (!vidSeen) {
|
||||
tagsList.push_back("_videos:yes");
|
||||
}
|
||||
if (!detSeen) {
|
||||
tagsList.push_back("_details:yes");
|
||||
}
|
||||
return tagsList;
|
||||
}
|
||||
|
||||
std::string kiwix::getTagValueFromTagList(
|
||||
const std::vector<std::string>& tagList, const std::string& tagName)
|
||||
{
|
||||
for (auto tag: tagList) {
|
||||
if (tag[0] == '_') {
|
||||
auto delimPos = tag.find(':');
|
||||
if (delimPos == std::string::npos) {
|
||||
// No delimiter... what to do ?
|
||||
continue;
|
||||
}
|
||||
auto cTagName = tag.substr(1, delimPos-1);
|
||||
auto cTagValue = tag.substr(delimPos+1);
|
||||
if (cTagName == tagName) {
|
||||
return cTagValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << tagName << " cannot be found";
|
||||
throw std::out_of_range(ss.str());
|
||||
}
|
||||
|
||||
bool kiwix::convertStrToBool(const std::string& value)
|
||||
{
|
||||
if (value == "yes") {
|
||||
return true;
|
||||
} else if (value == "no") {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "Tag value '" << value << "' cannot be converted to bool.";
|
||||
throw std::domain_error(ss.str());
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// The counter metadata format is a list of item separated by a `;` :
|
||||
// item0;item1;item2
|
||||
// Each item is a "tuple" mimetype=number.
|
||||
// However, the mimetype may contains parameters:
|
||||
// text/html;raw=true;foo=bar
|
||||
// So the final format may be complex to parse:
|
||||
// key0=value0;key1;foo=bar=value1;key2=value2
|
||||
|
||||
typedef kiwix::MimeCounterType::value_type MimetypeAndCounter;
|
||||
|
||||
std::string readFullMimetypeAndCounterString(std::istream& in)
|
||||
{
|
||||
std::string mtcStr, params;
|
||||
getline(in, mtcStr, ';');
|
||||
if ( mtcStr.find('=') == std::string::npos )
|
||||
{
|
||||
do
|
||||
{
|
||||
if ( !getline(in, params, ';' ) )
|
||||
return std::string();
|
||||
mtcStr += ";" + params;
|
||||
}
|
||||
while ( std::count(params.begin(), params.end(), '=') != 2 );
|
||||
}
|
||||
return mtcStr;
|
||||
}
|
||||
|
||||
MimetypeAndCounter parseASingleMimetypeCounter(const std::string& s)
|
||||
{
|
||||
const std::string::size_type k = s.find_last_of("=");
|
||||
if ( k != std::string::npos )
|
||||
{
|
||||
const std::string mimeType = s.substr(0, k);
|
||||
std::istringstream counterSS(s.substr(k+1));
|
||||
unsigned int counter;
|
||||
if (counterSS >> counter && counterSS.eof())
|
||||
return MimetypeAndCounter{mimeType, counter};
|
||||
}
|
||||
return MimetypeAndCounter{"", 0};
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
kiwix::MimeCounterType kiwix::parseMimetypeCounter(const std::string& counterData)
|
||||
{
|
||||
kiwix::MimeCounterType counters;
|
||||
std::istringstream ss(counterData);
|
||||
|
||||
while (ss)
|
||||
{
|
||||
const std::string mtcStr = readFullMimetypeAndCounterString(ss);
|
||||
const MimetypeAndCounter mtc = parseASingleMimetypeCounter(mtcStr);
|
||||
if ( !mtc.first.empty() )
|
||||
counters.insert(mtc);
|
||||
}
|
||||
|
||||
return counters;
|
||||
}
|
||||
|
||||
std::string kiwix::gen_date_str()
|
||||
{
|
||||
auto now = std::time(0);
|
||||
auto tm = std::localtime(&now);
|
||||
|
||||
std::stringstream is;
|
||||
is << std::setw(2) << std::setfill('0')
|
||||
<< 1900+tm->tm_year << "-"
|
||||
<< std::setw(2) << std::setfill('0') << tm->tm_mon+1 << "-"
|
||||
<< std::setw(2) << std::setfill('0') << tm->tm_mday << "T"
|
||||
<< std::setw(2) << std::setfill('0') << tm->tm_hour << ":"
|
||||
<< std::setw(2) << std::setfill('0') << tm->tm_min << ":"
|
||||
<< std::setw(2) << std::setfill('0') << tm->tm_sec << "Z";
|
||||
return is.str();
|
||||
}
|
||||
|
||||
std::string kiwix::gen_uuid(const std::string& s)
|
||||
{
|
||||
return kiwix::to_string(zim::Uuid::generate(s));
|
||||
}
|
||||
|
||||
std::string kiwix::render_template(const std::string& template_str, kainjow::mustache::data data)
|
||||
{
|
||||
kainjow::mustache::mustache tmpl(template_str);
|
||||
kainjow::mustache::data urlencode{kainjow::mustache::lambda2{
|
||||
[](const std::string& str,const kainjow::mustache::renderer& r) { return urlEncode(r(str), true); }}};
|
||||
data.set("urlencoded", urlencode);
|
||||
std::stringstream ss;
|
||||
tmpl.render(data, [&ss](const std::string& str) { ss << str; });
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "tools/pathTools.h"
|
||||
#include <stdexcept>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <limits.h>
|
||||
@@ -42,12 +43,16 @@
|
||||
#include <algorithm>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define SEPARATOR "\\"
|
||||
# define SEPARATOR "\\"
|
||||
# include <io.h>
|
||||
#else
|
||||
#define SEPARATOR "/"
|
||||
#include <unistd.h>
|
||||
# define SEPARATOR "/"
|
||||
# include <unistd.h>
|
||||
# include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifndef PATH_MAX
|
||||
@@ -76,13 +81,19 @@ std::wstring Utf8ToWide(const std::string& str)
|
||||
bool isRelativePath(const std::string& path)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return path.empty() || path.substr(1, 2) == ":\\" ? false : true;
|
||||
if (path.size() < 3 ) {
|
||||
return true;
|
||||
}
|
||||
if (path.substr(1, 2) == ":\\" || path.substr(0, 2) == "\\\\") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return path.empty() || path.substr(0, 1) == "/" ? false : true;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::vector<std::string> normalizeParts(std::vector<std::string> parts, bool absolute)
|
||||
std::vector<std::string> normalizeParts(std::vector<std::string>& parts, bool absolute)
|
||||
{
|
||||
std::vector<std::string> ret;
|
||||
#ifdef _WIN32
|
||||
@@ -90,10 +101,35 @@ std::vector<std::string> normalizeParts(std::vector<std::string> parts, bool abs
|
||||
//Starts from there.
|
||||
auto it = find_if(parts.rbegin(), parts.rend(),
|
||||
[](const std::string& p) ->bool
|
||||
{ return p.length() == 2 && p[1] == ':'; });
|
||||
{ return ((p.length() == 2 && p[1] == ':')
|
||||
|| (p.length() > 2 && p[0] == '\\' && p[1] == '\\')); });
|
||||
if (it != parts.rend()) {
|
||||
parts.erase(parts.begin(), it.base()-1);
|
||||
}
|
||||
// Special case for samba mount point starting with two "\\" ("\\\\samba\\foo")
|
||||
if (parts.size() > 2 && parts[0].empty() && parts[1].empty()) {
|
||||
parts.erase(parts.begin(), parts.begin()+2);
|
||||
parts[0] = "\\\\" + parts[0];
|
||||
}
|
||||
// Special case if we have a samba drive not at first.
|
||||
// Path is "..\\\\sambdadrive\\..\\.." So we will have an empty part.
|
||||
auto previous_empty = false;
|
||||
for (it = parts.rbegin(); it!=parts.rend(); it++) {
|
||||
if(it->empty()) {
|
||||
if (previous_empty) {
|
||||
it++;
|
||||
break;
|
||||
} else {
|
||||
previous_empty = true;
|
||||
}
|
||||
} else {
|
||||
previous_empty = false;
|
||||
}
|
||||
}
|
||||
if (it != parts.rend()) {
|
||||
parts.erase(parts.begin(), it.base()-1);
|
||||
parts[0] = "\\\\" + parts[0];
|
||||
}
|
||||
#endif
|
||||
|
||||
size_t index = 0;
|
||||
@@ -139,8 +175,10 @@ std::vector<std::string> normalizeParts(std::vector<std::string> parts, bool abs
|
||||
|
||||
std::string computeRelativePath(const std::string& path, const std::string& absolutePath)
|
||||
{
|
||||
auto pathParts = normalizeParts(kiwix::split(path, SEPARATOR, false), false);
|
||||
auto absolutePathParts = kiwix::split(absolutePath, SEPARATOR, false);
|
||||
auto parts = kiwix::split(path, SEPARATOR, false);
|
||||
auto pathParts = normalizeParts(parts, false);
|
||||
parts = kiwix::split(absolutePath, SEPARATOR, false);
|
||||
auto absolutePathParts = normalizeParts(parts, true);
|
||||
|
||||
unsigned int commonCount = 0;
|
||||
while (commonCount < pathParts.size()
|
||||
@@ -167,8 +205,10 @@ std::string computeAbsolutePath(const std::string& path, const std::string& rela
|
||||
absolutePath = getCurrentDirectory();
|
||||
}
|
||||
|
||||
auto absoluteParts = normalizeParts(kiwix::split(absolutePath, SEPARATOR, false), true);
|
||||
auto relativeParts = kiwix::split(relativePath, SEPARATOR, false);
|
||||
auto parts = kiwix::split(absolutePath, SEPARATOR, false);
|
||||
auto absoluteParts = normalizeParts(parts, true);
|
||||
parts = kiwix::split(relativePath, SEPARATOR, false);
|
||||
auto relativeParts = normalizeParts(parts, false);
|
||||
|
||||
absoluteParts.insert(absoluteParts.end(), relativeParts.begin(), relativeParts.end());
|
||||
auto ret = kiwix::join(normalizeParts(absoluteParts, true), SEPARATOR);
|
||||
@@ -177,7 +217,8 @@ std::string computeAbsolutePath(const std::string& path, const std::string& rela
|
||||
|
||||
std::string removeLastPathElement(const std::string& path)
|
||||
{
|
||||
auto parts = normalizeParts(kiwix::split(path, SEPARATOR, false), false);
|
||||
auto parts_ = kiwix::split(path, SEPARATOR, false);
|
||||
auto parts = normalizeParts(parts_, false);
|
||||
if (!parts.empty()) {
|
||||
parts.pop_back();
|
||||
}
|
||||
@@ -197,7 +238,8 @@ std::string appendToDirectory(const std::string& directoryPath, const std::strin
|
||||
|
||||
std::string getLastPathElement(const std::string& path)
|
||||
{
|
||||
auto parts = normalizeParts(kiwix::split(path, SEPARATOR), false);
|
||||
auto parts_ = kiwix::split(path, SEPARATOR);
|
||||
auto parts = normalizeParts(parts_, false);
|
||||
if (parts.empty()) {
|
||||
return "";
|
||||
}
|
||||
@@ -227,14 +269,33 @@ std::string getFileSizeAsString(const std::string& path)
|
||||
|
||||
std::string getFileContent(const std::string& path)
|
||||
{
|
||||
std::ifstream f(path, std::ios::in|std::ios::ate);
|
||||
#ifdef _WIN32
|
||||
auto wpath = Utf8ToWide(path);
|
||||
auto fd = _wopen(wpath.c_str(), _O_RDONLY | _O_BINARY);
|
||||
#else
|
||||
auto fd = open(path.c_str(), O_RDONLY);
|
||||
#endif
|
||||
std::string content;
|
||||
if (f.is_open()) {
|
||||
auto size = f.tellg();
|
||||
content.reserve(size);
|
||||
f.seekg(0, std::ios::beg);
|
||||
content.assign((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
if (fd != -1) {
|
||||
#ifdef _WIN32
|
||||
auto size = _lseeki64(fd, 0, SEEK_END);
|
||||
#else
|
||||
auto size = lseek(fd, 0, SEEK_END);
|
||||
#endif
|
||||
content.resize(size);
|
||||
#ifdef _WIN32
|
||||
_lseeki64(fd, 0, SEEK_SET);
|
||||
#else
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
#endif
|
||||
auto p = (char*)content.data();
|
||||
while (size) {
|
||||
auto readsize = size > 2048 ? 2048 : size;
|
||||
readsize = ::read(fd, p, readsize);
|
||||
p += readsize;
|
||||
size -= readsize;
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
@@ -278,7 +339,7 @@ std::string makeTmpDirectory()
|
||||
_wmkdir(ctmp);
|
||||
return WideToUtf8(ctmp);
|
||||
#else
|
||||
char _template_array[] = {"/tmp/kiwix-lib_XXXXXX"};
|
||||
char _template_array[] = {"/tmp/libkiwix_XXXXXX"};
|
||||
std::string dir = mkdtemp(_template_array);
|
||||
return dir;
|
||||
#endif
|
||||
@@ -287,22 +348,22 @@ std::string makeTmpDirectory()
|
||||
/* Try to create a link and if does not work then make a copy */
|
||||
bool copyFile(const std::string& sourcePath, const std::string& destPath)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return CopyFileW(Utf8ToWide(sourcePath).c_str(), Utf8ToWide(destPath).c_str(), 1);
|
||||
#else
|
||||
try {
|
||||
#ifndef _WIN32
|
||||
if (link(sourcePath.c_str(), destPath.c_str()) != 0) {
|
||||
#endif
|
||||
std::ifstream infile(sourcePath.c_str(), std::ios_base::binary);
|
||||
std::ofstream outfile(destPath.c_str(), std::ios_base::binary);
|
||||
outfile << infile.rdbuf();
|
||||
#ifndef _WIN32
|
||||
}
|
||||
#endif
|
||||
} catch (std::exception& e) {
|
||||
std::cerr << e.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string getExecutablePath(bool realPathOnly)
|
||||
@@ -341,10 +402,21 @@ std::string getExecutablePath(bool realPathOnly)
|
||||
|
||||
bool writeTextFile(const std::string& path, const std::string& content)
|
||||
{
|
||||
std::ofstream file;
|
||||
file.open(path.c_str());
|
||||
file << content;
|
||||
file.close();
|
||||
#ifdef _WIN32
|
||||
auto wpath = Utf8ToWide(path);
|
||||
auto fd = _wopen(wpath.c_str(), _O_WRONLY | _O_CREAT | _O_TRUNC, S_IWRITE);
|
||||
#else
|
||||
auto fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC,
|
||||
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||
#endif
|
||||
if (fd == -1)
|
||||
return false;
|
||||
|
||||
if (write(fd, content.c_str(), content.size()) != (long)content.size()) {
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
close(fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -364,30 +436,48 @@ std::string getCurrentDirectory()
|
||||
|
||||
std::string getDataDirectory()
|
||||
{
|
||||
// Try to get the dataDir from the `KIWIX_DATA_DIR` env var
|
||||
#ifdef _WIN32
|
||||
char* cDataDir = ::getenv("APPDATA");
|
||||
wchar_t* cDataDir = ::_wgetenv(L"KIWIX_DATA_DIR");
|
||||
if (cDataDir != nullptr) {
|
||||
return WideToUtf8(cDataDir);
|
||||
}
|
||||
#else
|
||||
char* cDataDir = ::getenv("KIWIX_DATA_DIR");
|
||||
#endif
|
||||
std::string dataDir = cDataDir==nullptr ? "" : cDataDir;
|
||||
if (!dataDir.empty()) {
|
||||
return dataDir;
|
||||
if (cDataDir != nullptr) {
|
||||
return cDataDir;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Compute the dataDir from the user directory.
|
||||
std::string dataDir;
|
||||
#ifdef _WIN32
|
||||
cDataDir = ::getenv("USERPROFILE");
|
||||
dataDir = cDataDir==nullptr ? getCurrentDirectory() : cDataDir;
|
||||
cDataDir = ::_wgetenv(L"APPDATA");
|
||||
if (cDataDir == nullptr)
|
||||
cDataDir = ::_wgetenv(L"USERPROFILE");
|
||||
if (cDataDir != nullptr)
|
||||
dataDir = WideToUtf8(cDataDir);
|
||||
#else
|
||||
cDataDir = ::getenv("XDG_DATA_HOME");
|
||||
dataDir = cDataDir==nullptr ? "" : cDataDir;
|
||||
if (dataDir.empty()) {
|
||||
if (cDataDir != nullptr) {
|
||||
dataDir = cDataDir;
|
||||
} else {
|
||||
cDataDir = ::getenv("HOME");
|
||||
dataDir = cDataDir==nullptr ? getCurrentDirectory() : cDataDir;
|
||||
dataDir = appendToDirectory(dataDir, ".local");
|
||||
dataDir = appendToDirectory(dataDir, "share");
|
||||
if (cDataDir != nullptr) {
|
||||
dataDir = cDataDir;
|
||||
dataDir = appendToDirectory(dataDir, ".local");
|
||||
dataDir = appendToDirectory(dataDir, "share");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
auto ret = appendToDirectory(dataDir, "kiwix");
|
||||
return ret;
|
||||
if (!dataDir.empty()) {
|
||||
dataDir = appendToDirectory(dataDir, "kiwix");
|
||||
makeDirectory(dataDir);
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
// Let's use the currentDirectory
|
||||
return getCurrentDirectory();
|
||||
}
|
||||
|
||||
static std::map<std::string, std::string> extMimeTypes = {
|
||||
|
||||
@@ -18,17 +18,17 @@
|
||||
*/
|
||||
|
||||
#include <tools/regexTools.h>
|
||||
#include <tools/lock.h>
|
||||
|
||||
#include <unicode/regex.h>
|
||||
#include <unicode/ucnv.h>
|
||||
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <pthread.h>
|
||||
#include <stdexcept>
|
||||
#include <mutex>
|
||||
|
||||
std::map<std::string, std::shared_ptr<icu::RegexPattern>> regexCache;
|
||||
static pthread_mutex_t regexLock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static std::mutex regexLock;
|
||||
|
||||
std::unique_ptr<icu::RegexMatcher> buildMatcher(const std::string& regex, icu::UnicodeString& content)
|
||||
{
|
||||
@@ -38,7 +38,7 @@ std::unique_ptr<icu::RegexMatcher> buildMatcher(const std::string& regex, icu::U
|
||||
pattern = regexCache.at(regex);
|
||||
} catch (std::out_of_range&) {
|
||||
// Redo the search with a lock to avoid race condition.
|
||||
kiwix::Lock l(®exLock);
|
||||
std::lock_guard<std::mutex> l(regexLock);
|
||||
try {
|
||||
pattern = regexCache.at(regex);
|
||||
} catch (std::out_of_range&) {
|
||||
@@ -94,3 +94,22 @@ std::string appendToFirstOccurence(const std::string& content,
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
std::string prependToFirstOccurence(const std::string& content,
|
||||
const std::string& regex,
|
||||
const std::string& replacement)
|
||||
{
|
||||
ucnv_setDefaultName("UTF-8");
|
||||
icu::UnicodeString ucontent(content.c_str());
|
||||
icu::UnicodeString ureplacement(replacement.c_str());
|
||||
auto matcher = buildMatcher(regex, ucontent);
|
||||
if (matcher->find()) {
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
ucontent.insert(matcher->start(status), ureplacement);
|
||||
std::string tmp;
|
||||
ucontent.toUTF8String(tmp);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -268,7 +268,8 @@ std::string kiwix::urlDecode(const std::string& value, bool component)
|
||||
/* Split string in a token array */
|
||||
std::vector<std::string> kiwix::split(const std::string& str,
|
||||
const std::string& delims,
|
||||
bool trimEmpty)
|
||||
bool trimEmpty,
|
||||
bool keepDelim)
|
||||
{
|
||||
std::string::size_type lastPos = 0;
|
||||
std::string::size_type pos = 0;
|
||||
@@ -279,6 +280,9 @@ std::vector<std::string> kiwix::split(const std::string& str,
|
||||
if (!trimEmpty || !token.empty()) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
if (keepDelim) {
|
||||
tokens.push_back(str.substr(pos, 1));
|
||||
}
|
||||
lastPos = pos + 1;
|
||||
}
|
||||
|
||||
|
||||
88
src/wrapper/java/book.cpp
Normal file
88
src/wrapper/java/book.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#include <jni.h>
|
||||
#include "org_kiwix_kiwixlib_Book.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "book.h"
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_kiwix_kiwixlib_Book_allocate(
|
||||
JNIEnv* env, jobject thisObj)
|
||||
{
|
||||
allocate<kiwix::Book>(env, thisObj);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_kiwix_kiwixlib_Book_dispose(JNIEnv* env, jobject thisObj)
|
||||
{
|
||||
dispose<kiwix::Book>(env, thisObj);
|
||||
}
|
||||
|
||||
#define BOOK (getPtr<kiwix::Book>(env, thisObj))
|
||||
|
||||
METHOD(void, Book, update__Lorg_kiwix_kiwixlib_Book_2, jobject otherBook)
|
||||
{
|
||||
BOOK->update(*getPtr<kiwix::Book>(env, otherBook));
|
||||
}
|
||||
|
||||
METHOD(void, Book, update__Lorg_kiwix_kiwixlib_JNIKiwixReader_2, jobject reader)
|
||||
{
|
||||
BOOK->update(**Handle<kiwix::Reader>::getHandle(env, reader));
|
||||
}
|
||||
|
||||
#define GETTER(retType, name) JNIEXPORT retType JNICALL \
|
||||
Java_org_kiwix_kiwixlib_Book_##name (JNIEnv* env, jobject thisObj) \
|
||||
{ \
|
||||
auto cRet = BOOK->name(); \
|
||||
retType ret = c2jni(cRet, env); \
|
||||
return ret; \
|
||||
}
|
||||
|
||||
GETTER(jstring, getId)
|
||||
GETTER(jstring, getPath)
|
||||
GETTER(jboolean, isPathValid)
|
||||
GETTER(jstring, getTitle)
|
||||
GETTER(jstring, getDescription)
|
||||
GETTER(jstring, getLanguage)
|
||||
GETTER(jstring, getCreator)
|
||||
GETTER(jstring, getPublisher)
|
||||
GETTER(jstring, getDate)
|
||||
GETTER(jstring, getUrl)
|
||||
GETTER(jstring, getName)
|
||||
GETTER(jstring, getFlavour)
|
||||
GETTER(jstring, getCategory)
|
||||
GETTER(jstring, getTags)
|
||||
GETTER(jlong, getArticleCount)
|
||||
GETTER(jlong, getMediaCount)
|
||||
GETTER(jlong, getSize)
|
||||
GETTER(jstring, getFavicon)
|
||||
GETTER(jstring, getFaviconUrl)
|
||||
GETTER(jstring, getFaviconMimeType)
|
||||
|
||||
METHOD(jstring, Book, getTagStr, jstring tagName) try {
|
||||
auto cRet = BOOK->getTagStr(jni2c(tagName, env));
|
||||
return c2jni(cRet, env);
|
||||
} catch(...) {
|
||||
return c2jni<std::string>("", env);
|
||||
}
|
||||
|
||||
#undef GETTER
|
||||
63
src/wrapper/java/filter.cpp
Normal file
63
src/wrapper/java/filter.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) 2019-2020 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#include <jni.h>
|
||||
#include "org_kiwix_kiwixlib_Filter.h"
|
||||
|
||||
#include "library.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* Kiwix Reader JNI functions */
|
||||
METHOD0(void, Filter, allocate) {
|
||||
allocate<kiwix::Filter>(env, thisObj);
|
||||
}
|
||||
|
||||
METHOD0(void, Filter, dispose) {
|
||||
dispose<kiwix::Library>(env, thisObj);
|
||||
}
|
||||
|
||||
#define FILTER (getPtr<kiwix::Filter>(env, thisObj))
|
||||
|
||||
#define FORWARD(name, args_type) \
|
||||
METHOD(jobject, Filter, name, args_type value) { \
|
||||
FILTER->name(jni2c(value, env)); \
|
||||
return thisObj; \
|
||||
}
|
||||
|
||||
#define FORWARDA(name, args_type) \
|
||||
METHOD(jobject, Filter, name, jobjectArray value) { \
|
||||
FILTER->name(jni2c<args_type>(value, env)); \
|
||||
return thisObj; \
|
||||
}
|
||||
|
||||
|
||||
|
||||
FORWARD(local, jboolean)
|
||||
FORWARD(remote, jboolean)
|
||||
FORWARD(valid, jboolean)
|
||||
FORWARDA(acceptTags, jstring)
|
||||
FORWARDA(rejectTags, jstring)
|
||||
FORWARD(lang, jstring)
|
||||
FORWARD(publisher, jstring)
|
||||
FORWARD(creator, jstring)
|
||||
FORWARD(maxSize, jlong)
|
||||
FORWARD(query, jstring)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
pthread_mutex_t globalLock = PTHREAD_RECURSIVE_MUTEX_INITIALIZER;
|
||||
std::mutex globalLock;
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_kiwix_kiwixlib_JNIICU_setDataDirectory(
|
||||
JNIEnv* env, jclass kclass, jstring dirStr)
|
||||
@@ -20,8 +20,7 @@
|
||||
|
||||
|
||||
#include <jni.h>
|
||||
#include <zim/file.h>
|
||||
#include <android/log.h>
|
||||
#include <exception>
|
||||
#include "org_kiwix_kiwixlib_JNIKiwixReader.h"
|
||||
|
||||
#include "tools/base64.h"
|
||||
@@ -34,18 +33,84 @@ JNIEXPORT jlong JNICALL Java_org_kiwix_kiwixlib_JNIKiwixReader_getNativeReader(
|
||||
{
|
||||
std::string cPath = jni2c(filename, env);
|
||||
|
||||
__android_log_print(ANDROID_LOG_INFO, "kiwix", "Attempting to create reader with: %s", cPath.c_str());
|
||||
LOG("Attempting to create reader with: %s", cPath.c_str());
|
||||
Lock l;
|
||||
try {
|
||||
kiwix::Reader* reader = new kiwix::Reader(cPath);
|
||||
return reinterpret_cast<jlong>(new Handle<kiwix::Reader>(reader));
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", "Error opening ZIM file");
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", e.what());
|
||||
LOG("Error opening ZIM file");
|
||||
LOG(e.what());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int jni2fd(const jobject& fdObj, JNIEnv* env)
|
||||
{
|
||||
jclass class_fdesc = env->FindClass("java/io/FileDescriptor");
|
||||
jfieldID field_fd = env->GetFieldID(class_fdesc, "fd", "I");
|
||||
if ( field_fd == NULL )
|
||||
{
|
||||
env->ExceptionClear();
|
||||
// Under Android the (private) 'fd' field of java.io.FileDescriptor has been
|
||||
// renamed to 'descriptor'. See, for example,
|
||||
// https://android.googlesource.com/platform/libcore/+/refs/tags/android-8.1.0_r1/ojluni/src/main/java/java/io/FileDescriptor.java#55
|
||||
field_fd = env->GetFieldID(class_fdesc, "descriptor", "I");
|
||||
}
|
||||
return env->GetIntField(fdObj, field_fd);
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
JNIEXPORT jlong JNICALL Java_org_kiwix_kiwixlib_JNIKiwixReader_getNativeReaderByFD(
|
||||
JNIEnv* env, jobject obj, jobject fdObj)
|
||||
{
|
||||
#ifndef _WIN32
|
||||
int fd = jni2fd(fdObj, env);
|
||||
|
||||
LOG("Attempting to create reader with fd: %d", fd);
|
||||
Lock l;
|
||||
try {
|
||||
kiwix::Reader* reader = new kiwix::Reader(fd);
|
||||
return reinterpret_cast<jlong>(new Handle<kiwix::Reader>(reader));
|
||||
} catch (std::exception& e) {
|
||||
LOG("Error opening ZIM file");
|
||||
LOG(e.what());
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
jclass exception = env->FindClass("java/lang/UnsupportedOperationException");
|
||||
env->ThrowNew(exception, "org.kiwix.kiwixlib.JNIKiwixReader.getNativeReaderByFD() is not supported under Windows");
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL Java_org_kiwix_kiwixlib_JNIKiwixReader_getNativeReaderEmbedded(
|
||||
JNIEnv* env, jobject obj, jobject fdObj, jlong offset, jlong size)
|
||||
{
|
||||
#ifndef _WIN32
|
||||
int fd = jni2fd(fdObj, env);
|
||||
|
||||
LOG("Attempting to create reader with fd: %d", fd);
|
||||
Lock l;
|
||||
try {
|
||||
kiwix::Reader* reader = new kiwix::Reader(fd, offset, size);
|
||||
return reinterpret_cast<jlong>(new Handle<kiwix::Reader>(reader));
|
||||
} catch (std::exception& e) {
|
||||
LOG("Error opening ZIM file");
|
||||
LOG(e.what());
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
jclass exception = env->FindClass("java/lang/UnsupportedOperationException");
|
||||
env->ThrowNew(exception, "org.kiwix.kiwixlib.JNIKiwixReader.getNativeReaderEmbedded() is not supported under Windows");
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_kiwix_kiwixlib_JNIKiwixReader_dispose(JNIEnv* env, jobject obj)
|
||||
{
|
||||
@@ -64,8 +129,8 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getMainPage(JNIEnv* env, jobject obj)
|
||||
std::string cUrl = READER->getMainPage().getPath();
|
||||
url = c2jni(cUrl, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get ZIM main page");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get ZIM main page");
|
||||
LOG(e.what());
|
||||
url = NULL;
|
||||
}
|
||||
return url;
|
||||
@@ -80,8 +145,8 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getId(JNIEnv* env, jobject obj)
|
||||
std::string cId = READER->getId();
|
||||
id = c2jni(cId, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get ZIM id");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get ZIM id");
|
||||
LOG(e.what());
|
||||
id = NULL;
|
||||
}
|
||||
|
||||
@@ -95,10 +160,10 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getFileSize(JNIEnv* env, jobject obj)
|
||||
|
||||
try {
|
||||
int cSize = READER->getFileSize();
|
||||
size = c2jni(cSize);
|
||||
size = c2jni(cSize, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get ZIM file size");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get ZIM file size");
|
||||
LOG(e.what());
|
||||
}
|
||||
|
||||
return size;
|
||||
@@ -113,8 +178,8 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getCreator(JNIEnv* env, jobject obj)
|
||||
std::string cCreator = READER->getCreator();
|
||||
creator = c2jni(cCreator, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get ZIM creator");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get ZIM creator");
|
||||
LOG(e.what());
|
||||
creator = NULL;
|
||||
}
|
||||
|
||||
@@ -130,8 +195,8 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getPublisher(JNIEnv* env, jobject obj)
|
||||
std::string cPublisher = READER->getPublisher();
|
||||
publisher = c2jni(cPublisher, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get ZIM publish");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get ZIM publish");
|
||||
LOG(e.what());
|
||||
publisher = NULL;
|
||||
}
|
||||
return publisher;
|
||||
@@ -146,8 +211,8 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getName(JNIEnv* env, jobject obj)
|
||||
std::string cName = READER->getName();
|
||||
name = c2jni(cName, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get ZIM name");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get ZIM name");
|
||||
LOG(e.what());
|
||||
name = NULL;
|
||||
}
|
||||
return name;
|
||||
@@ -166,8 +231,8 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getFavicon(JNIEnv* env, jobject obj)
|
||||
base64_encode(cContent),
|
||||
env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get ZIM favicon");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get ZIM favicon");
|
||||
LOG(e.what());
|
||||
favicon = NULL;
|
||||
}
|
||||
return favicon;
|
||||
@@ -182,8 +247,8 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getDate(JNIEnv* env, jobject obj)
|
||||
std::string cDate = READER->getDate();
|
||||
date = c2jni(cDate, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get ZIM date");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get ZIM date");
|
||||
LOG(e.what());
|
||||
date = NULL;
|
||||
}
|
||||
return date;
|
||||
@@ -198,8 +263,8 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getLanguage(JNIEnv* env, jobject obj)
|
||||
std::string cLanguage = READER->getLanguage();
|
||||
language = c2jni(cLanguage, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get ZIM language");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get ZIM language");
|
||||
LOG(e.what());
|
||||
language = NULL;
|
||||
}
|
||||
|
||||
@@ -217,8 +282,8 @@ JNIEXPORT jstring JNICALL Java_org_kiwix_kiwixlib_JNIKiwixReader_getMimeType(
|
||||
auto cMimeType = entry.getMimetype();
|
||||
mimeType = c2jni(cMimeType, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get mime-type for url: %s", cUrl.c_str());
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get mime-type for url: %s", cUrl.c_str());
|
||||
LOG(e.what());
|
||||
mimeType = NULL;
|
||||
}
|
||||
return mimeType;
|
||||
@@ -234,7 +299,7 @@ JNIEXPORT jstring JNICALL Java_org_kiwix_kiwixlib_JNIKiwixReader_checkUrl(
|
||||
entry = entry.getFinalEntry();
|
||||
finalUrl = c2jni(entry.getPath(), env);
|
||||
} catch (std::exception& e) {
|
||||
finalUrl = c2jni("", env);
|
||||
finalUrl = c2jni(std::string(), env);
|
||||
}
|
||||
return finalUrl;
|
||||
}
|
||||
@@ -268,8 +333,8 @@ JNIEXPORT jbyteArray JNICALL Java_org_kiwix_kiwixlib_JNIKiwixReader_getContent(
|
||||
data, 0, cSize, reinterpret_cast<const jbyte*>(entry.getBlob().data()));
|
||||
}
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get content for url: %s", cUrl.c_str());
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get content for url: %s", cUrl.c_str());
|
||||
LOG(e.what());
|
||||
}
|
||||
|
||||
return data;
|
||||
@@ -284,8 +349,8 @@ JNIEXPORT jbyteArray JNICALL Java_org_kiwix_kiwixlib_JNIKiwixReader_getContentPa
|
||||
/* Default values */
|
||||
/* Retrieve the content */
|
||||
std::string cUrl = jni2c(url, env);
|
||||
unsigned int cOffset = jni2c(offset);
|
||||
unsigned int cLen = jni2c(len);
|
||||
unsigned int cOffset = jni2c(offset, env);
|
||||
unsigned int cLen = jni2c(len, env);
|
||||
try {
|
||||
auto entry = READER->getEntryFromEncodedPath(cUrl);
|
||||
entry = entry.getFinalEntry();
|
||||
@@ -300,32 +365,48 @@ JNIEXPORT jbyteArray JNICALL Java_org_kiwix_kiwixlib_JNIKiwixReader_getContentPa
|
||||
setIntObjValue(cLen, sizeObj, env);
|
||||
}
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get partial content for url: %s (%u : %u)", cUrl.c_str(), cOffset, cLen);
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get partial content for url: %s (%u : %u)", cUrl.c_str(), cOffset, cLen);
|
||||
LOG(e.what());
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_org_kiwix_kiwixlib_JNIKiwixReader_getArticleSize(
|
||||
JNIEnv* env, jobject obj, jstring url)
|
||||
{
|
||||
std::string cUrl = jni2c(url, env);
|
||||
try {
|
||||
auto entry = READER->getEntryFromEncodedPath(cUrl);
|
||||
entry = entry.getFinalEntry();
|
||||
return c2jni(entry.getSize(), env);
|
||||
} catch(std::exception& e) {
|
||||
LOG("Unable to get size for url : %s", cUrl.c_str());
|
||||
LOG(e.what());
|
||||
}
|
||||
return c2jni(0, env);
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_org_kiwix_kiwixlib_JNIKiwixReader_getDirectAccessInformation(
|
||||
JNIEnv* env, jobject obj, jstring url)
|
||||
{
|
||||
jclass classPair = env->FindClass("org/kiwix/kiwixlib/Pair");
|
||||
jmethodID midPairinit = env->GetMethodID(classPair, "<init>", "()V");
|
||||
jobject pair = env->NewObject(classPair, midPairinit);
|
||||
setPairObjValue("", 0, pair, env);
|
||||
jclass daiClass = env->FindClass("org/kiwix/kiwixlib/DirectAccessInfo");
|
||||
jmethodID daiInitMethod = env->GetMethodID(daiClass, "<init>", "()V");
|
||||
jobject dai = env->NewObject(daiClass, daiInitMethod);
|
||||
setDaiObjValue("", 0, dai, env);
|
||||
|
||||
std::string cUrl = jni2c(url, env);
|
||||
try {
|
||||
auto entry = READER->getEntryFromEncodedPath(cUrl);
|
||||
entry = entry.getFinalEntry();
|
||||
auto part_info = entry.getDirectAccessInfo();
|
||||
setPairObjValue(part_info.first, part_info.second, pair, env);
|
||||
setDaiObjValue(part_info.first, part_info.second, dai, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get direct access info for url: %s", cUrl.c_str());
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get direct access info for url: %s", cUrl.c_str());
|
||||
LOG(e.what());
|
||||
}
|
||||
return pair;
|
||||
return dai;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
@@ -336,15 +417,18 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_searchSuggestions(JNIEnv* env,
|
||||
{
|
||||
jboolean retVal = JNI_FALSE;
|
||||
std::string cPrefix = jni2c(prefix, env);
|
||||
unsigned int cCount = jni2c(count);
|
||||
unsigned int cCount = jni2c(count, env);
|
||||
|
||||
try {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
if (READER->searchSuggestionsSmart(cPrefix, cCount)) {
|
||||
retVal = JNI_TRUE;
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", "Unable to get search results for pattern: %s", cPrefix.c_str());
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", e.what());
|
||||
LOG("Unable to get search results for pattern: %s", cPrefix.c_str());
|
||||
LOG(e.what());
|
||||
}
|
||||
|
||||
return retVal;
|
||||
@@ -361,14 +445,17 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getNextSuggestion(JNIEnv* env,
|
||||
std::string cUrl;
|
||||
|
||||
try {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
if (READER->getNextSuggestion(cTitle, cUrl)) {
|
||||
setStringObjValue(cTitle, titleObj, env);
|
||||
setStringObjValue(cUrl, urlObj, env);
|
||||
retVal = JNI_TRUE;
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", "Unable to get next suggestion");
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", e.what());
|
||||
LOG("Unable to get next suggestion");
|
||||
LOG(e.what());
|
||||
}
|
||||
|
||||
return retVal;
|
||||
@@ -388,8 +475,8 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getPageUrlFromTitle(JNIEnv* env,
|
||||
setStringObjValue(entry.getPath(), urlObj, env);
|
||||
return JNI_TRUE;
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", "Unable to get url for title %s: ", cTitle.c_str());
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", e.what());
|
||||
LOG("Unable to get url for title %s: ", cTitle.c_str());
|
||||
LOG(e.what());
|
||||
}
|
||||
|
||||
return JNI_FALSE;
|
||||
@@ -404,8 +491,8 @@ JNIEXPORT jstring JNICALL Java_org_kiwix_kiwixlib_JNIKiwixReader_getTitle(
|
||||
std::string cTitle = READER->getTitle();
|
||||
title = c2jni(cTitle, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get zim title");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get zim title");
|
||||
LOG(e.what());
|
||||
title = NULL;
|
||||
}
|
||||
return title;
|
||||
@@ -420,8 +507,8 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getDescription(JNIEnv* env, jobject obj)
|
||||
std::string cDescription = READER->getDescription();
|
||||
description = c2jni(cDescription, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get zim description");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get zim description");
|
||||
LOG(e.what());
|
||||
description = NULL;
|
||||
}
|
||||
return description;
|
||||
@@ -433,10 +520,10 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getArticleCount(JNIEnv* env, jobject obj)
|
||||
jint articleCount = 0;
|
||||
try {
|
||||
auto cArticleCount = READER->getArticleCount();
|
||||
articleCount = c2jni(cArticleCount);
|
||||
articleCount = c2jni(cArticleCount, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get article count.");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get article count.");
|
||||
LOG(e.what());
|
||||
}
|
||||
return articleCount;
|
||||
}
|
||||
@@ -447,10 +534,10 @@ Java_org_kiwix_kiwixlib_JNIKiwixReader_getMediaCount(JNIEnv* env, jobject obj)
|
||||
jint mediaCount = 0;
|
||||
try {
|
||||
auto cMediaCount = READER->getMediaCount();
|
||||
mediaCount = c2jni(cMediaCount);
|
||||
mediaCount = c2jni(cMediaCount, env);
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get media count.");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get media count.");
|
||||
LOG(e.what());
|
||||
}
|
||||
return mediaCount;
|
||||
}
|
||||
@@ -467,8 +554,8 @@ JNIEXPORT jboolean JNICALL Java_org_kiwix_kiwixlib_JNIKiwixReader_getRandomPage(
|
||||
setStringObjValue(cUrl, urlObj, env);
|
||||
retVal = JNI_TRUE;
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", "Unable to get random page");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "kiwix", e.what());
|
||||
LOG("Unable to get random page");
|
||||
LOG(e.what());
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
*/
|
||||
|
||||
|
||||
#include <zim/file.h>
|
||||
#include "org_kiwix_kiwixlib_JNIKiwixSearcher.h"
|
||||
#include "org_kiwix_kiwixlib_JNIKiwixSearcher_Result.h"
|
||||
|
||||
@@ -59,7 +58,7 @@ JNIEXPORT void JNICALL Java_org_kiwix_kiwixlib_JNIKiwixSearcher_search(
|
||||
JNIEnv* env, jobject obj, jstring query, jint count)
|
||||
{
|
||||
std::string cquery = jni2c(query, env);
|
||||
unsigned int ccount = jni2c(count);
|
||||
unsigned int ccount = jni2c(count, env);
|
||||
|
||||
SEARCHER->search(cquery, 0, ccount);
|
||||
}
|
||||
@@ -20,8 +20,6 @@
|
||||
|
||||
|
||||
#include <jni.h>
|
||||
#include <zim/file.h>
|
||||
#include <android/log.h>
|
||||
#include "org_kiwix_kiwixlib_JNIKiwixServer.h"
|
||||
|
||||
#include "tools/base64.h"
|
||||
@@ -32,15 +30,15 @@
|
||||
JNIEXPORT jlong JNICALL Java_org_kiwix_kiwixlib_JNIKiwixServer_getNativeServer(
|
||||
JNIEnv* env, jobject obj, jobject jLibrary)
|
||||
{
|
||||
__android_log_print(ANDROID_LOG_INFO, "kiwix", "Attempting to create server");
|
||||
LOG("Attempting to create server");
|
||||
Lock l;
|
||||
try {
|
||||
auto library = Handle<kiwix::Library>::getHandle(env, jLibrary);
|
||||
kiwix::Server* server = new kiwix::Server(*library);
|
||||
auto library = getPtr<kiwix::Library>(env, jLibrary);
|
||||
kiwix::Server* server = new kiwix::Server(library);
|
||||
return reinterpret_cast<jlong>(new Handle<kiwix::Server>(server));
|
||||
} catch (std::exception& e) {
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", "Error creating the server");
|
||||
__android_log_print(ANDROID_LOG_WARN, "kiwix", e.what());
|
||||
LOG("Error creating the server");
|
||||
LOG(e.what());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -86,6 +84,12 @@ Java_org_kiwix_kiwixlib_JNIKiwixServer_setTaskbar(JNIEnv* env, jobject obj, jboo
|
||||
SERVER->setTaskbar(withTaskbar, withLibraryButton);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_kiwix_kiwixlib_JNIKiwixServer_setBlockExternalLinks(JNIEnv* env, jobject obj, jboolean blockExternalLinks)
|
||||
{
|
||||
SERVER->setBlockExternalLinks(blockExternalLinks);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_kiwix_kiwixlib_JNIKiwixServer_start(JNIEnv* env, jobject obj)
|
||||
{
|
||||
96
src/wrapper/java/library.cpp
Normal file
96
src/wrapper/java/library.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2019-2020 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#include <jni.h>
|
||||
#include "org_kiwix_kiwixlib_Library.h"
|
||||
|
||||
#include "library.h"
|
||||
#include "reader.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* Kiwix Reader JNI functions */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_kiwix_kiwixlib_Library_allocate(
|
||||
JNIEnv* env, jobject thisObj)
|
||||
{
|
||||
allocate<kiwix::Library>(env, thisObj);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_kiwix_kiwixlib_Library_dispose(JNIEnv* env, jobject thisObj)
|
||||
{
|
||||
dispose<kiwix::Library>(env, thisObj);
|
||||
}
|
||||
|
||||
#define LIBRARY (getPtr<kiwix::Library>(env, thisObj))
|
||||
|
||||
/* Kiwix library functions */
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_kiwix_kiwixlib_Library_addBook(
|
||||
JNIEnv* env, jobject thisObj, jstring path)
|
||||
{
|
||||
auto cPath = jni2c(path, env);
|
||||
|
||||
try {
|
||||
kiwix::Reader reader(cPath);
|
||||
kiwix::Book book;
|
||||
book.update(reader);
|
||||
return LIBRARY->addBook(book);
|
||||
} catch (std::exception& e) {
|
||||
LOG("Unable to add the book");
|
||||
LOG(e.what()); }
|
||||
return false;
|
||||
}
|
||||
|
||||
METHOD(jobject, Library, getBookById, jstring id) {
|
||||
auto cId = jni2c(id, env);
|
||||
auto cBook = new kiwix::Book(LIBRARY->getBookById(cId));
|
||||
jclass cls = env->FindClass("org/kiwix/kiwixlib/Book");
|
||||
jmethodID constructorId = env->GetMethodID(cls, "<init>", "()V");
|
||||
jobject book = env->NewObject(cls, constructorId);
|
||||
setPtr(env, book, cBook);
|
||||
return book;
|
||||
}
|
||||
|
||||
METHOD(jint, Library, getBookCount, jboolean localBooks, jboolean remoteBooks) {
|
||||
return LIBRARY->getBookCount(localBooks, remoteBooks);
|
||||
}
|
||||
|
||||
METHOD0(jobjectArray, Library, getBooksIds) {
|
||||
return c2jni(LIBRARY->getBooksIds(), env);
|
||||
}
|
||||
|
||||
METHOD(jobjectArray, Library, filter, jobject filterObj) {
|
||||
auto filter = getPtr<kiwix::Filter>(env, filterObj);
|
||||
return c2jni(LIBRARY->filter(*filter), env);
|
||||
}
|
||||
|
||||
METHOD0(jobjectArray, Library, getBooksLanguages) {
|
||||
return c2jni(LIBRARY->getBooksLanguages(), env);
|
||||
}
|
||||
|
||||
METHOD0(jobjectArray, Library, getBooksCreators) {
|
||||
return c2jni(LIBRARY->getBooksCreators(), env);
|
||||
}
|
||||
|
||||
METHOD0(jobjectArray, Library, getBooksPublisher) {
|
||||
return c2jni(LIBRARY->getBooksPublishers(), env);
|
||||
}
|
||||
|
||||
131
src/wrapper/java/manager.cpp
Normal file
131
src/wrapper/java/manager.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#include <jni.h>
|
||||
#include "org_kiwix_kiwixlib_Manager.h"
|
||||
|
||||
#include "manager.h"
|
||||
#include "utils.h"
|
||||
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_kiwix_kiwixlib_Manager_allocate(
|
||||
JNIEnv* env, jobject thisObj, jobject libraryObj)
|
||||
{
|
||||
auto lib = getPtr<kiwix::Library>(env, libraryObj);
|
||||
allocate<kiwix::Manager>(env, thisObj, lib);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_kiwix_kiwixlib_Manager_dispose(JNIEnv* env, jobject thisObj)
|
||||
{
|
||||
dispose<kiwix::Manager>(env, thisObj);
|
||||
}
|
||||
|
||||
#define MANAGER (getPtr<kiwix::Manager>(env, thisObj))
|
||||
|
||||
/* Kiwix manager functions */
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_kiwix_kiwixlib_Manager_readFile(
|
||||
JNIEnv* env, jobject thisObj, jstring path)
|
||||
{
|
||||
auto cPath = jni2c(path, env);
|
||||
|
||||
try {
|
||||
return MANAGER->readFile(cPath);
|
||||
} catch (std::exception& e) {
|
||||
LOG("Unable to get readFile");
|
||||
LOG(e.what());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_kiwix_kiwixlib_Manager_readXml(
|
||||
JNIEnv* env, jobject thisObj, jstring content, jstring libraryPath)
|
||||
{
|
||||
auto cContent = jni2c(content, env);
|
||||
auto cPath = jni2c(libraryPath, env);
|
||||
|
||||
try {
|
||||
return MANAGER->readXml(cContent, false, cPath);
|
||||
} catch (std::exception& e) {
|
||||
LOG("Unable to get ZIM id");
|
||||
LOG(e.what());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_kiwix_kiwixlib_Manager_readOpds(
|
||||
JNIEnv* env, jobject thisObj, jstring content, jstring urlHost)
|
||||
{
|
||||
auto cContent = jni2c(content, env);
|
||||
auto cUrl = jni2c(urlHost, env);
|
||||
|
||||
try {
|
||||
return MANAGER->readOpds(cContent, cUrl);
|
||||
} catch (std::exception& e) {
|
||||
LOG("Unable to get ZIM id");
|
||||
LOG(e.what());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_kiwix_kiwixlib_Manager_readBookmarkFile(
|
||||
JNIEnv* env, jobject thisObj, jstring path)
|
||||
{
|
||||
auto cPath = jni2c(path, env);
|
||||
|
||||
try {
|
||||
return MANAGER->readBookmarkFile(cPath);
|
||||
} catch (std::exception& e) {
|
||||
LOG("Unable to get ZIM id");
|
||||
LOG(e.what());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_org_kiwix_kiwixlib_Manager_addBookFromPath(
|
||||
JNIEnv* env, jobject thisObj,
|
||||
jstring pathToOpen, jstring pathToSave, jstring url, jboolean checkMetaData)
|
||||
{
|
||||
auto cPathToOpen = jni2c(pathToOpen, env);
|
||||
auto cPathToSave = jni2c(pathToSave, env);
|
||||
auto cUrl = jni2c(url, env);
|
||||
jstring id = NULL;
|
||||
|
||||
try {
|
||||
auto cId = MANAGER->addBookFromPathAndGetId(cPathToOpen, cPathToSave, cUrl, checkMetaData);
|
||||
if ( !cId.empty() ) {
|
||||
id = c2jni(cId, env);
|
||||
}
|
||||
} catch (std::exception& e) {
|
||||
LOG("Unable to get ZIM file size");
|
||||
LOG(e.what());
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
55
src/wrapper/java/meson.build
Normal file
55
src/wrapper/java/meson.build
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
java_sources = files([
|
||||
'org/kiwix/kiwixlib/JNIICU.java',
|
||||
'org/kiwix/kiwixlib/Book.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixReader.java',
|
||||
'org/kiwix/kiwixlib/Library.java',
|
||||
'org/kiwix/kiwixlib/Manager.java',
|
||||
'org/kiwix/kiwixlib/Filter.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixSearcher.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixServer.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixInt.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixString.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixBool.java',
|
||||
'org/kiwix/kiwixlib/JNIKiwixException.java',
|
||||
'org/kiwix/kiwixlib/DirectAccessInfo.java'
|
||||
])
|
||||
|
||||
kiwix_jni = custom_target('jni',
|
||||
input: java_sources,
|
||||
output: ['org_kiwix_kiwixlib_JNIKiwix.h',
|
||||
'org_kiwix_kiwixlib_Book.h',
|
||||
'org_kiwix_kiwixlib_JNIKiwixReader.h',
|
||||
'org_kiwix_kiwixlib_Library.h',
|
||||
'org_kiwix_kiwixlib_Manager.h',
|
||||
'org_kiwix_kiwixlib_Filter.h',
|
||||
'org_kiwix_kiwixlib_JNIKiwixServer.h',
|
||||
'org_kiwix_kiwixlib_JNIKiwixSearcher.h',
|
||||
'org_kiwix_kiwixlib_JNIKiwixSearcher_Result.h'],
|
||||
command:['javac', '-d', '@OUTDIR@', '-h', '@OUTDIR@', '@INPUT@']
|
||||
)
|
||||
|
||||
jni_sources = files([
|
||||
'kiwixicu.cpp',
|
||||
'book.cpp',
|
||||
'kiwixreader.cpp',
|
||||
'library.cpp',
|
||||
'manager.cpp',
|
||||
'filter.cpp',
|
||||
'kiwixsearcher.cpp',
|
||||
'kiwixserver.cpp',
|
||||
])
|
||||
|
||||
kiwix_sources += jni_sources + [kiwix_jni]
|
||||
|
||||
if 'java' in wrapper
|
||||
kiwix_jar = jar('kiwixlib', java_sources)
|
||||
#junit_jar = files('org/kiwix/testing/junit-4.13.jar')
|
||||
#test_jar = jar('testing', 'org/kiwix/testing/test.java',
|
||||
# link_with: [kiwix_jar, junit_jar])
|
||||
#test('javatest', test_jar)
|
||||
endif
|
||||
|
||||
install_subdir('org', install_dir: 'kiwix-lib/java', exclude_directories: ['kiwix/testing'])
|
||||
install_subdir('res', install_dir: 'kiwix-lib')
|
||||
install_data('AndroidManifest.xml', install_dir: 'kiwix-lib')
|
||||
48
src/wrapper/java/org/kiwix/kiwixlib/Book.java
Normal file
48
src/wrapper/java/org/kiwix/kiwixlib/Book.java
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
package org.kiwix.kiwixlib;
|
||||
|
||||
public class Book
|
||||
{
|
||||
public Book() { allocate(); }
|
||||
|
||||
|
||||
public native void update(Book book);
|
||||
public native void update(JNIKiwixReader reader);
|
||||
|
||||
@Override
|
||||
protected void finalize() { dispose(); }
|
||||
|
||||
public native String getId();
|
||||
public native String getPath();
|
||||
public native boolean isPathValid();
|
||||
public native String getTitle();
|
||||
public native String getDescription();
|
||||
public native String getLanguage();
|
||||
public native String getCreator();
|
||||
public native String getPublisher();
|
||||
public native String getDate();
|
||||
public native String getUrl();
|
||||
public native String getName();
|
||||
public native String getFlavour();
|
||||
public native String getCategory();
|
||||
public native String getTags();
|
||||
/**
|
||||
* Return the value associated to the tag tagName
|
||||
*
|
||||
* @param tagName the tag name to search for.
|
||||
* @return The value of the tag. If the tag is not found, return empty string.
|
||||
*/
|
||||
public native String getTagStr(String tagName);
|
||||
|
||||
public native long getArticleCount();
|
||||
public native long getMediaCount();
|
||||
public native long getSize();
|
||||
|
||||
public native String getFavicon();
|
||||
public native String getFaviconUrl();
|
||||
public native String getFaviconMimeType();
|
||||
|
||||
private native void allocate();
|
||||
private native void dispose();
|
||||
private long nativeHandle;
|
||||
}
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
package org.kiwix.kiwixlib;
|
||||
|
||||
public class Pair
|
||||
public class DirectAccessInfo
|
||||
{
|
||||
public String filename;
|
||||
public int offset;
|
||||
public long offset;
|
||||
}
|
||||
44
src/wrapper/java/org/kiwix/kiwixlib/Filter.java
Normal file
44
src/wrapper/java/org/kiwix/kiwixlib/Filter.java
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2019-2020 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixlib;
|
||||
|
||||
public class Filter
|
||||
{
|
||||
|
||||
public native Filter local(boolean accept);
|
||||
public native Filter remote(boolean accept);
|
||||
public native Filter valid(boolean accept);
|
||||
public native Filter acceptTags(String[] tags);
|
||||
public native Filter rejectTags(String[] tags);
|
||||
public native Filter lang(String lang);
|
||||
public native Filter publisher(String publisher);
|
||||
public native Filter creator(String creator);
|
||||
public native Filter maxSize(long size);
|
||||
public native Filter query(String query);
|
||||
|
||||
|
||||
public Filter() { allocate(); }
|
||||
|
||||
@Override
|
||||
protected void finalize() { dispose(); }
|
||||
private native void allocate();
|
||||
private native void dispose();
|
||||
private long nativeHandle;
|
||||
}
|
||||
@@ -24,7 +24,8 @@ import org.kiwix.kiwixlib.JNIKiwixException;
|
||||
import org.kiwix.kiwixlib.JNIKiwixString;
|
||||
import org.kiwix.kiwixlib.JNIKiwixInt;
|
||||
import org.kiwix.kiwixlib.JNIKiwixSearcher;
|
||||
import org.kiwix.kiwixlib.Pair;
|
||||
import org.kiwix.kiwixlib.DirectAccessInfo;
|
||||
import java.io.FileDescriptor;
|
||||
|
||||
public class JNIKiwixReader
|
||||
{
|
||||
@@ -82,6 +83,16 @@ public class JNIKiwixReader
|
||||
int len,
|
||||
JNIKiwixInt size);
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the size of an article.
|
||||
*
|
||||
* @param url The url of the article.
|
||||
* @return The size of the final (redirections are resolved) article (in byte).
|
||||
* Return 0 if the article is not found.
|
||||
*/
|
||||
public native long getArticleSize(String url);
|
||||
|
||||
/**
|
||||
* getDirectAccessInformation.
|
||||
*
|
||||
@@ -92,13 +103,13 @@ public class JNIKiwixReader
|
||||
* the zim file (or zim part) and directly read the content from it (and so
|
||||
* bypassing the libzim).
|
||||
*
|
||||
* Return a `Pair` (filename, offset) where the content is located.
|
||||
* Return a `DirectAccessInfo` (filename, offset) where the content is located.
|
||||
*
|
||||
* If the content cannot be directly accessed (content is compressed or zim
|
||||
* file is cut in the middle of the content), the filename is an empty string
|
||||
* and offset is zero.
|
||||
*/
|
||||
public native Pair getDirectAccessInformation(String url);
|
||||
public native DirectAccessInfo getDirectAccessInformation(String url);
|
||||
|
||||
public native boolean searchSuggestions(String prefix, int count);
|
||||
|
||||
@@ -141,11 +152,31 @@ public class JNIKiwixReader
|
||||
throw new JNIKiwixException("Cannot open zimfile "+filename);
|
||||
}
|
||||
}
|
||||
|
||||
public JNIKiwixReader(FileDescriptor fd) throws JNIKiwixException
|
||||
{
|
||||
nativeHandle = getNativeReaderByFD(fd);
|
||||
if (nativeHandle == 0) {
|
||||
throw new JNIKiwixException("Cannot open zimfile by fd "+fd.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public JNIKiwixReader(FileDescriptor fd, long offset, long size)
|
||||
throws JNIKiwixException
|
||||
{
|
||||
nativeHandle = getNativeReaderEmbedded(fd, offset, size);
|
||||
if (nativeHandle == 0) {
|
||||
throw new JNIKiwixException(String.format("Cannot open embedded zimfile (fd=%s, offset=%d, size=%d)", fd, offset, size));
|
||||
}
|
||||
}
|
||||
|
||||
public JNIKiwixReader() {
|
||||
|
||||
}
|
||||
public native void dispose();
|
||||
|
||||
private native long getNativeReader(String filename);
|
||||
private native long getNativeReaderByFD(FileDescriptor fd);
|
||||
private native long getNativeReaderEmbedded(FileDescriptor fd, long offset, long size);
|
||||
private long nativeHandle;
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
package org.kiwix.kiwixlib;
|
||||
|
||||
import org.kiwix.kiwixlib.JNIKiwixException;
|
||||
import org.kiwix.kiwixlib.JNIKiwixLibrary;
|
||||
import org.kiwix.kiwixlib.Library;
|
||||
|
||||
public class JNIKiwixServer
|
||||
{
|
||||
@@ -34,15 +34,17 @@ public class JNIKiwixServer
|
||||
|
||||
public native void setTaskbar(boolean withTaskBar, boolean witLibraryButton);
|
||||
|
||||
public native void setBlockExternalLinks(boolean blockExternalLinks);
|
||||
|
||||
public native boolean start();
|
||||
|
||||
public native void stop();
|
||||
|
||||
public JNIKiwixServer(JNIKiwixLibrary library)
|
||||
public JNIKiwixServer(Library library)
|
||||
{
|
||||
nativeHandle = getNativeServer(library);
|
||||
}
|
||||
|
||||
private native long getNativeServer(JNIKiwixLibrary library);
|
||||
private native long getNativeServer(Library library);
|
||||
private long nativeHandle;
|
||||
}
|
||||
49
src/wrapper/java/org/kiwix/kiwixlib/Library.java
Normal file
49
src/wrapper/java/org/kiwix/kiwixlib/Library.java
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2019-2020 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixlib;
|
||||
|
||||
import org.kiwix.kiwixlib.Book;
|
||||
import org.kiwix.kiwixlib.JNIKiwixException;
|
||||
|
||||
public class Library
|
||||
{
|
||||
public native boolean addBook(String path) throws JNIKiwixException;
|
||||
|
||||
public native Book getBookById(String id);
|
||||
public native int getBookCount(boolean localBooks, boolean remoteBooks);
|
||||
|
||||
public native String[] getBooksIds();
|
||||
public native String[] filter(Filter filter);
|
||||
|
||||
public native String[] getBooksLanguages();
|
||||
public native String[] getBooksCreators();
|
||||
public native String[] getBooksPublishers();
|
||||
|
||||
public Library()
|
||||
{
|
||||
allocate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() { dispose(); }
|
||||
private native void allocate();
|
||||
private native void dispose();
|
||||
private long nativeHandle;
|
||||
}
|
||||
90
src/wrapper/java/org/kiwix/kiwixlib/Manager.java
Normal file
90
src/wrapper/java/org/kiwix/kiwixlib/Manager.java
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixlib;
|
||||
|
||||
import org.kiwix.kiwixlib.Library;
|
||||
|
||||
public class Manager
|
||||
{
|
||||
/**
|
||||
* Read a `library.xml` file and add books in the library.
|
||||
*
|
||||
* @param path The (utf8) path to the `library.xml` file.
|
||||
* @return True if the file has been properly parsed.
|
||||
*/
|
||||
public native boolean readFile(String path);
|
||||
|
||||
/**
|
||||
* Load a library content stored in a string (at `library.xml` format).
|
||||
*
|
||||
* @param content The content corresponding of the library xml.
|
||||
* @param libraryPath The library path (used to resolve relative paths)
|
||||
* @return True if the content has been properly parsed.
|
||||
*/
|
||||
public native boolean readXml(String content, String libraryPath);
|
||||
|
||||
/**
|
||||
* Load a library content stored in a string (at OPDS stream format)
|
||||
*
|
||||
* @param content the content of the OPDS stream.
|
||||
* @param urlHost the url of the stream (used to resolve relative url)
|
||||
* @return True if the content has been properly parsed.
|
||||
*/
|
||||
public native boolean readOpds(String content, String urlHost);
|
||||
|
||||
/**
|
||||
* Load a bookmark file
|
||||
*
|
||||
* @param path The path of the file to read.
|
||||
* @return True if the content has been properly parsed
|
||||
*/
|
||||
public native boolean readBookmarkFile(String path);
|
||||
|
||||
/**
|
||||
* Add a book to the library.
|
||||
*
|
||||
* @param pathToOpen The path of the zim file to add.
|
||||
* @param pathToSave The path to store in the library in place of
|
||||
* pathToOpen.
|
||||
* @param url The url of the book to store in the library
|
||||
* (useful for kiiwix-serve catalog)
|
||||
* @param checkMetaData Tell if we check metadata before adding a book to the
|
||||
* library.
|
||||
* @return The id of te book if the book has been added to the library.
|
||||
* Empty string if not.
|
||||
*/
|
||||
public native String addBookFromPath(String pathToOpen,
|
||||
String pathToSave,
|
||||
String url,
|
||||
boolean checkMetaData);
|
||||
|
||||
public Manager(Library library) {
|
||||
allocate(library);
|
||||
_library = library;
|
||||
}
|
||||
|
||||
private Library _library;
|
||||
|
||||
@Override
|
||||
protected void finalize() { dispose(); }
|
||||
private native void allocate(Library library);
|
||||
private native void dispose();
|
||||
private long nativeHandle;
|
||||
}
|
||||
19
src/wrapper/java/org/kiwix/testing/catalog.xml
Normal file
19
src/wrapper/java/org/kiwix/testing/catalog.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog">
|
||||
<id>00000000-0000-0000-0000-000000000000</id>
|
||||
<entry>
|
||||
<title>Test ZIM file</title>
|
||||
<id>urn:uuid:86c91e51-55bf-8882-464e-072aca37a3e8</id>
|
||||
<icon>/meta?name=favicon&content=small</icon>
|
||||
<updated>2020-11-27:00::00:Z</updated>
|
||||
<language>en</language>
|
||||
<summary>This is a ZIM file used in libzim unit-tests</summary>
|
||||
<tags>unit;test</tags>
|
||||
<link type="text/html" href="/small" />
|
||||
<author>
|
||||
<name>Kiwix</name>
|
||||
</author>
|
||||
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://localhost/small.zim" length="78982" />
|
||||
<link rel="http://opds-spec.org/image/thumbnail" type="image/png" href="/meta?name=favicon&content=small" />
|
||||
</entry>
|
||||
</feed>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user