weblib: fix delete and imprt
[fanfix.git] / src / be / nikiroo / fanfix / library / WebLibraryServer.java
CommitLineData
f433d153
NR
1package be.nikiroo.fanfix.library;
2
3import java.io.ByteArrayInputStream;
f433d153
NR
4import java.io.IOException;
5import java.io.InputStream;
f70bcacf 6import java.net.URL;
f433d153 7import java.util.ArrayList;
5c4ce687 8import java.util.Arrays;
f433d153
NR
9import java.util.HashMap;
10import java.util.LinkedList;
11import java.util.List;
12import java.util.Map;
13
f433d153
NR
14import org.json.JSONArray;
15import org.json.JSONObject;
16
17import be.nikiroo.fanfix.Instance;
18import be.nikiroo.fanfix.bundles.Config;
f433d153
NR
19import be.nikiroo.fanfix.data.Chapter;
20import be.nikiroo.fanfix.data.JsonIO;
21import be.nikiroo.fanfix.data.MetaData;
22import be.nikiroo.fanfix.data.Paragraph;
23import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
24import be.nikiroo.fanfix.data.Story;
f433d153 25import be.nikiroo.utils.Image;
fce0a73f 26import be.nikiroo.utils.LoginResult;
f433d153 27import be.nikiroo.utils.NanoHTTPD;
f433d153
NR
28import be.nikiroo.utils.NanoHTTPD.Response;
29import be.nikiroo.utils.NanoHTTPD.Response.Status;
f70bcacf 30import be.nikiroo.utils.Progress;
f433d153 31
33d40b9f
NR
32public class WebLibraryServer extends WebLibraryServerHtml {
33 class WLoginResult extends LoginResult {
fce0a73f
NR
34 public WLoginResult(boolean badLogin, boolean badCookie) {
35 super(badLogin, badCookie);
36 }
37
38 public WLoginResult(String who, String key, String subkey, boolean rw,
39 boolean wl, boolean bl) {
40 super(who, key, subkey, (rw ? "|rw" : "") + (wl ? "|wl" : "")
41 + (bl ? "|bl" : "") + "|");
f433d153
NR
42 }
43
fce0a73f 44 public WLoginResult(String cookie, String who, String key,
f433d153 45 List<String> subkeys) {
fce0a73f
NR
46 super(cookie, who, key, subkeys,
47 subkeys == null || subkeys.isEmpty());
f433d153
NR
48 }
49
50 public boolean isRw() {
fce0a73f 51 return getOption().contains("|rw|");
f433d153
NR
52 }
53
54 public boolean isWl() {
fce0a73f 55 return getOption().contains("|wl|");
f433d153
NR
56 }
57
d11fb35b 58 public boolean isBl() {
fce0a73f 59 return getOption().contains("|bl|");
f433d153
NR
60 }
61 }
62
f433d153
NR
63 private Map<String, Story> storyCache = new HashMap<String, Story>();
64 private LinkedList<String> storyCacheOrder = new LinkedList<String>();
65 private long storyCacheSize = 0;
66 private long maxStoryCacheSize;
f433d153 67
d11fb35b
NR
68 private List<String> whitelist;
69 private List<String> blacklist;
70
f70bcacf
NR
71 private Map<String, Progress> imprts = new HashMap<String, Progress>();
72
f433d153 73 public WebLibraryServer(boolean secure) throws IOException {
33d40b9f 74 super(secure);
f433d153
NR
75
76 int cacheMb = Instance.getInstance().getConfig()
77 .getInteger(Config.SERVER_MAX_CACHE_MB, 100);
78 maxStoryCacheSize = cacheMb * 1024 * 1024;
79
80 setTraceHandler(Instance.getInstance().getTraceHandler());
81
d11fb35b
NR
82 whitelist = Instance.getInstance().getConfig()
83 .getList(Config.SERVER_WHITELIST, new ArrayList<String>());
84 blacklist = Instance.getInstance().getConfig()
85 .getList(Config.SERVER_BLACKLIST, new ArrayList<String>());
f433d153
NR
86 }
87
88 /**
89 * Start the server (listen on the network for new connections).
90 * <p>
91 * Can only be called once.
92 * <p>
93 * This call is asynchronous, and will just start a new {@link Thread} on
94 * itself (see {@link WebLibraryServer#run()}).
95 */
96 public void start() {
97 new Thread(this).start();
98 }
99
33d40b9f
NR
100 @Override
101 protected WLoginResult login(boolean badLogin, boolean badCookie) {
102 return new WLoginResult(false, false);
f433d153
NR
103 }
104
33d40b9f
NR
105 @Override
106 protected WLoginResult login(String who, String cookie) {
fce0a73f
NR
107 List<String> subkeys = Instance.getInstance().getConfig()
108 .getList(Config.SERVER_ALLOWED_SUBKEYS);
f433d153 109 String realKey = Instance.getInstance().getConfig()
fce0a73f 110 .getString(Config.SERVER_KEY);
d11fb35b 111
fce0a73f 112 return new WLoginResult(cookie, who, realKey, subkeys);
f433d153
NR
113 }
114
115 // allow rw/wl
33d40b9f
NR
116 @Override
117 protected WLoginResult login(String who, String key, String subkey) {
f433d153 118 String realKey = Instance.getInstance().getConfig()
d11fb35b 119 .getString(Config.SERVER_KEY, "");
f433d153
NR
120
121 // I don't like NULLs...
f433d153
NR
122 key = key == null ? "" : key;
123 subkey = subkey == null ? "" : subkey;
124
125 if (!realKey.equals(key)) {
fce0a73f 126 return new WLoginResult(true, false);
f433d153
NR
127 }
128
d11fb35b 129 // defaults are true (as previous versions without the feature)
f433d153
NR
130 boolean rw = true;
131 boolean wl = true;
d11fb35b 132 boolean bl = true;
f433d153 133
599b05c7
NR
134 rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
135 rw);
fce0a73f
NR
136
137 List<String> allowed = Instance.getInstance().getConfig().getList(
138 Config.SERVER_ALLOWED_SUBKEYS, new ArrayList<String>());
139
140 if (!allowed.isEmpty()) {
141 if (!allowed.contains(subkey)) {
142 return new WLoginResult(true, false);
143 }
144
145 if ((subkey + "|").contains("|rw|")) {
146 rw = true;
147 }
148 if ((subkey + "|").contains("|wl|")) {
149 wl = false; // |wl| = bypass whitelist
150 }
151 if ((subkey + "|").contains("|bl|")) {
152 bl = false; // |bl| = bypass blacklist
f433d153
NR
153 }
154 }
155
fce0a73f 156 return new WLoginResult(who, key, subkey, rw, wl, bl);
f433d153
NR
157 }
158
33d40b9f 159 @Override
fce0a73f 160 protected Response getList(String uri, WLoginResult login)
f433d153 161 throws IOException {
5ee0fc14 162 if (WebLibraryUrls.LIST_URL_METADATA.equals(uri)) {
f433d153 163 List<JSONObject> jsons = new ArrayList<JSONObject>();
d11fb35b 164 for (MetaData meta : metas(login)) {
f433d153
NR
165 jsons.add(JsonIO.toJson(meta));
166 }
167
168 return newInputStreamResponse("application/json",
599b05c7
NR
169 new ByteArrayInputStream(
170 new JSONArray(jsons).toString().getBytes()));
f433d153
NR
171 }
172
173 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
174 NanoHTTPD.MIME_PLAINTEXT, null);
175 }
176
f433d153
NR
177 // /story/luid/chapter/para <-- text/image
178 // /story/luid/cover <-- image
179 // /story/luid/metadata <-- json
c5103223 180 // /story/luid/json <-- json, whole chapter (no images)
33d40b9f
NR
181 @Override
182 protected Response getStoryPart(String uri, WLoginResult login) {
6673ec59 183 String[] uriParts = uri.split("/");
f433d153
NR
184 int off = 2;
185
6673ec59 186 if (uriParts.length < off + 2) {
f433d153
NR
187 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
188 NanoHTTPD.MIME_PLAINTEXT, null);
189 }
190
6673ec59
NR
191 String luid = uriParts[off + 0];
192 String chapterStr = uriParts[off + 1];
193 String imageStr = uriParts.length < off + 3 ? null : uriParts[off + 2];
f433d153
NR
194
195 // 1-based (0 = desc)
196 int chapter = 0;
197 if (chapterStr != null && !"cover".equals(chapterStr)
599b05c7
NR
198 && !"metadata".equals(chapterStr)
199 && !"json".equals(chapterStr)) {
f433d153
NR
200 try {
201 chapter = Integer.parseInt(chapterStr);
202 if (chapter < 0) {
203 throw new NumberFormatException();
204 }
205 } catch (NumberFormatException e) {
206 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
207 NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
208 }
209 }
210
211 // 1-based
212 int paragraph = 1;
213 if (imageStr != null) {
214 try {
215 paragraph = Integer.parseInt(imageStr);
216 if (paragraph < 0) {
217 throw new NumberFormatException();
218 }
219 } catch (NumberFormatException e) {
220 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
221 NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
222 }
223 }
224
225 String mimeType = NanoHTTPD.MIME_PLAINTEXT;
226 InputStream in = null;
227 try {
228 if ("cover".equals(chapterStr)) {
6673ec59 229 Image img = storyCover(luid, login);
f433d153
NR
230 if (img != null) {
231 in = img.newInputStream();
232 }
3f468ac7
NR
233 // TODO: get correct image type
234 mimeType = "image/png";
f433d153 235 } else if ("metadata".equals(chapterStr)) {
d11fb35b 236 MetaData meta = meta(luid, login);
f433d153
NR
237 JSONObject json = JsonIO.toJson(meta);
238 mimeType = "application/json";
239 in = new ByteArrayInputStream(json.toString().getBytes());
3fbc084c 240 } else if ("json".equals(chapterStr)) {
d11fb35b 241 Story story = story(luid, login);
c5103223
NR
242 JSONObject json = JsonIO.toJson(story);
243 mimeType = "application/json";
244 in = new ByteArrayInputStream(json.toString().getBytes());
f433d153 245 } else {
d11fb35b 246 Story story = story(luid, login);
f433d153
NR
247 if (story != null) {
248 if (chapter == 0) {
249 StringBuilder builder = new StringBuilder();
250 for (Paragraph p : story.getMeta().getResume()) {
251 if (builder.length() == 0) {
252 builder.append("\n");
253 }
254 builder.append(p.getContent());
255 }
256
599b05c7
NR
257 in = new ByteArrayInputStream(
258 builder.toString().getBytes("utf-8"));
f433d153
NR
259 } else {
260 Paragraph para = story.getChapters().get(chapter - 1)
261 .getParagraphs().get(paragraph - 1);
262 Image img = para.getContentImage();
263 if (para.getType() == ParagraphType.IMAGE) {
264 // TODO: get correct image type
265 mimeType = "image/png";
266 in = img.newInputStream();
267 } else {
599b05c7
NR
268 in = new ByteArrayInputStream(
269 para.getContent().getBytes("utf-8"));
f433d153
NR
270 }
271 }
272 }
273 }
274 } catch (IndexOutOfBoundsException e) {
275 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
276 NanoHTTPD.MIME_PLAINTEXT,
277 "Chapter or paragraph does not exist");
278 } catch (IOException e) {
279 Instance.getInstance().getTraceHandler()
280 .error(new IOException("Cannot get image: " + uri, e));
281 return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
282 NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
283 }
284
285 return newInputStreamResponse(mimeType, in);
286 }
287
089e354e
NR
288 // /story/luid/source
289 // /story/luid/title
290 // /story/luid/author
291 @Override
292 protected Response setStoryPart(String uri, String value,
293 WLoginResult login) throws IOException {
294 String[] uriParts = uri.split("/");
295 int off = 2; // "" and "story"
296
297 if (uriParts.length < off + 2) {
298 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
299 NanoHTTPD.MIME_PLAINTEXT, "Invalid story part request");
300 }
301
a1226ce0
NR
302 if (!login.isRw()) {
303 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
304 NanoHTTPD.MIME_PLAINTEXT, "SET story part not allowed");
305 }
306
089e354e
NR
307 String luid = uriParts[off + 0];
308 String type = uriParts[off + 1];
309
310 if (!Arrays.asList("source", "title", "author").contains(type)) {
311 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
312 NanoHTTPD.MIME_PLAINTEXT,
313 "Invalid SET story part: " + type);
314 }
315
316 if (meta(luid, login) != null) {
317 BasicLibrary lib = Instance.getInstance().getLibrary();
318 if ("source".equals(type)) {
319 lib.changeSource(luid, value, null);
320 } else if ("title".equals(type)) {
321 lib.changeTitle(luid, value, null);
322 } else if ("author".equals(type)) {
323 lib.changeAuthor(luid, value, null);
324 }
325 }
326
327 return newInputStreamResponse(NanoHTTPD.MIME_PLAINTEXT, null);
328 }
329
6673ec59
NR
330 @Override
331 protected Response getCover(String uri, WLoginResult login)
332 throws IOException {
333 String[] uriParts = uri.split("/");
334 int off = 2; // "" and "cover"
335
336 if (uriParts.length < off + 2) {
337 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
338 NanoHTTPD.MIME_PLAINTEXT, "Invalid cover request");
339 }
340
341 String type = uriParts[off + 0];
342 String id = uriParts[off + 1];
343
344 InputStream in = null;
345
e4b1b70c 346 if ("story".equals(type)) {
6673ec59
NR
347 Image img = storyCover(id, login);
348 if (img != null) {
349 in = img.newInputStream();
350 }
e4b1b70c
NR
351 } else if ("source".equals(type)) {
352 Image img = sourceCover(id, login);
6673ec59
NR
353 if (img != null) {
354 in = img.newInputStream();
355 }
e4b1b70c
NR
356 } else if ("author".equals(type)) {
357 Image img = authorCover(id, login);
6673ec59
NR
358 if (img != null) {
359 in = img.newInputStream();
360 }
361 } else {
362 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
e4b1b70c
NR
363 NanoHTTPD.MIME_PLAINTEXT,
364 "Invalid GET cover type: " + type);
6673ec59
NR
365 }
366
367 // TODO: get correct image type
368 return newInputStreamResponse("image/png", in);
369 }
370
e4b1b70c
NR
371 @Override
372 protected Response setCover(String uri, String luid, WLoginResult login)
373 throws IOException {
374 String[] uriParts = uri.split("/");
375 int off = 2; // "" and "cover"
376
377 if (uriParts.length < off + 2) {
378 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
379 NanoHTTPD.MIME_PLAINTEXT, "Invalid cover request");
380 }
381
a1226ce0
NR
382 if (!login.isRw()) {
383 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
384 NanoHTTPD.MIME_PLAINTEXT, "Cover request not allowed");
385 }
386
e4b1b70c
NR
387 String type = uriParts[off + 0];
388 String id = uriParts[off + 1];
389
390 if ("source".equals(type)) {
391 sourceCover(id, login, luid);
392 } else if ("author".equals(type)) {
393 authorCover(id, login, luid);
394 } else {
395 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
396 NanoHTTPD.MIME_PLAINTEXT,
397 "Invalid SET cover type: " + type);
398 }
399
400 return newInputStreamResponse(NanoHTTPD.MIME_PLAINTEXT, null);
401 }
402
f70bcacf
NR
403 @Override
404 protected Response imprt(String uri, String urlStr, WLoginResult login)
405 throws IOException {
406 final BasicLibrary lib = Instance.getInstance().getLibrary();
407
a1226ce0
NR
408 if (!login.isRw()) {
409 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
410 NanoHTTPD.MIME_PLAINTEXT, "Import not allowed");
411 }
412
f70bcacf
NR
413 final URL url = new URL(urlStr);
414 final Progress pg = new Progress();
415 final String luid = lib.getNextId();
416
417 synchronized (imprts) {
418 imprts.put(luid, pg);
419 }
420
421 new Thread(new Runnable() {
422 @Override
423 public void run() {
424 try {
acbec0d2 425 lib.imprt(url, luid, pg);
f70bcacf
NR
426 } catch (IOException e) {
427 Instance.getInstance().getTraceHandler().error(e);
428 } finally {
429 synchronized (imprts) {
430 imprts.remove(luid);
431 }
432 }
433 }
434 }, "Import story: " + urlStr).start();
435
f70bcacf
NR
436 return NanoHTTPD.newFixedLengthResponse(Status.OK,
437 NanoHTTPD.MIME_PLAINTEXT, luid);
438 }
439
440 @Override
441 protected Response imprtProgress(String uri, WLoginResult login) {
442 String[] uriParts = uri.split("/");
443 int off = 2; // "" and "import"
444
445 if (uriParts.length < off + 1) {
446 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
447 NanoHTTPD.MIME_PLAINTEXT, "Invalid cover request");
448 }
449
450 String luid = uriParts[off + 0];
451
452 Progress pg = null;
453 synchronized (imprts) {
454 pg = imprts.get(luid);
455 }
456 if (pg != null) {
457 return NanoHTTPD.newFixedLengthResponse(Status.OK,
458 "application/json", JsonIO.toJson(pg).toString());
459 }
460
461 return newInputStreamResponse(NanoHTTPD.MIME_PLAINTEXT, null);
462 }
463
a1226ce0
NR
464 @Override
465 protected Response delete(String uri, WLoginResult login)
466 throws IOException {
467 String[] uriParts = uri.split("/");
468 int off = 2; // "" and "delete"
469
470 if (uriParts.length < off + 1) {
471 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
472 NanoHTTPD.MIME_PLAINTEXT, "Invalid delete request");
473 }
474
475 if (!login.isRw()) {
476 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
477 NanoHTTPD.MIME_PLAINTEXT, "Delete not allowed");
478 }
479
480 String luid = uriParts[off + 0];
481
482 BasicLibrary lib = Instance.getInstance().getLibrary();
483 lib.delete(luid);
484
485 return newInputStreamResponse(NanoHTTPD.MIME_PLAINTEXT, null);
486 }
487
33d40b9f
NR
488 @Override
489 protected List<MetaData> metas(WLoginResult login) throws IOException {
d11fb35b
NR
490 BasicLibrary lib = Instance.getInstance().getLibrary();
491 List<MetaData> metas = new ArrayList<MetaData>();
492 for (MetaData meta : lib.getList().getMetas()) {
493 if (isAllowed(meta, login)) {
494 metas.add(meta);
495 }
496 }
497
498 return metas;
499 }
500
f433d153 501 // NULL if not whitelist OK or if not found
33d40b9f
NR
502 @Override
503 protected Story story(String luid, WLoginResult login) throws IOException {
f433d153
NR
504 synchronized (storyCache) {
505 if (storyCache.containsKey(luid)) {
506 Story story = storyCache.get(luid);
d11fb35b 507 if (!isAllowed(story.getMeta(), login))
f433d153 508 return null;
f433d153
NR
509
510 return story;
511 }
512 }
513
514 Story story = null;
d11fb35b 515 MetaData meta = meta(luid, login);
f433d153
NR
516 if (meta != null) {
517 BasicLibrary lib = Instance.getInstance().getLibrary();
518 story = lib.getStory(luid, null);
519 long size = sizeOf(story);
520
521 synchronized (storyCache) {
522 // Could have been added by another request
523 if (!storyCache.containsKey(luid)) {
524 while (!storyCacheOrder.isEmpty()
525 && storyCacheSize + size > maxStoryCacheSize) {
526 String oldestLuid = storyCacheOrder.removeFirst();
527 Story oldestStory = storyCache.remove(oldestLuid);
528 maxStoryCacheSize -= sizeOf(oldestStory);
529 }
530
531 storyCacheOrder.add(luid);
532 storyCache.put(luid, story);
533 }
534 }
535 }
536
537 return story;
538 }
539
33d40b9f
NR
540 private MetaData meta(String luid, WLoginResult login) throws IOException {
541 BasicLibrary lib = Instance.getInstance().getLibrary();
542 MetaData meta = lib.getInfo(luid);
543 if (!isAllowed(meta, login))
544 return null;
f433d153 545
33d40b9f 546 return meta;
f433d153
NR
547 }
548
6673ec59
NR
549 private Image storyCover(String luid, WLoginResult login)
550 throws IOException {
33d40b9f
NR
551 MetaData meta = meta(luid, login);
552 if (meta != null) {
553 BasicLibrary lib = Instance.getInstance().getLibrary();
554 return lib.getCover(meta.getLuid());
f433d153 555 }
f433d153 556
33d40b9f 557 return null;
f433d153
NR
558 }
559
6673ec59
NR
560 private Image authorCover(String author, WLoginResult login)
561 throws IOException {
562 Image img = null;
563
564 List<MetaData> metas = new MetaResultList(metas(login)).filter(null,
565 author, null);
566 if (metas.size() > 0) {
567 BasicLibrary lib = Instance.getInstance().getLibrary();
568 img = lib.getCustomAuthorCover(author);
569 if (img == null)
570 img = lib.getCover(metas.get(0).getLuid());
571 }
572
573 return img;
574
575 }
576
e4b1b70c
NR
577 private void authorCover(String author, WLoginResult login, String luid)
578 throws IOException {
579 if (meta(luid, login) != null) {
580 List<MetaData> metas = new MetaResultList(metas(login)).filter(null,
581 author, null);
582 if (metas.size() > 0) {
583 BasicLibrary lib = Instance.getInstance().getLibrary();
584 lib.setAuthorCover(author, luid);
585 }
586 }
587 }
588
6673ec59
NR
589 private Image sourceCover(String source, WLoginResult login)
590 throws IOException {
591 Image img = null;
592
593 List<MetaData> metas = new MetaResultList(metas(login)).filter(source,
594 null, null);
595 if (metas.size() > 0) {
596 BasicLibrary lib = Instance.getInstance().getLibrary();
597 img = lib.getCustomSourceCover(source);
598 if (img == null)
599 img = lib.getCover(metas.get(0).getLuid());
600 }
601
602 return img;
603 }
604
e4b1b70c
NR
605 private void sourceCover(String source, WLoginResult login, String luid)
606 throws IOException {
607 if (meta(luid, login) != null) {
608 List<MetaData> metas = new MetaResultList(metas(login))
609 .filter(source, null, null);
610 if (metas.size() > 0) {
611 BasicLibrary lib = Instance.getInstance().getLibrary();
612 lib.setSourceCover(source, luid);
613 }
614 }
615 }
616
33d40b9f 617 private boolean isAllowed(MetaData meta, WLoginResult login) {
5c4ce687
NR
618 MetaResultList one = new MetaResultList(Arrays.asList(meta));
619 if (login.isWl() && !whitelist.isEmpty()) {
620 if (one.filter(whitelist, null, null).isEmpty()) {
621 return false;
622 }
f433d153 623 }
5c4ce687
NR
624 if (login.isBl() && !blacklist.isEmpty()) {
625 if (!one.filter(blacklist, null, null).isEmpty()) {
626 return false;
627 }
599b05c7
NR
628 }
629
33d40b9f 630 return true;
599b05c7
NR
631 }
632
33d40b9f
NR
633 private long sizeOf(Story story) {
634 long size = 0;
635 for (Chapter chap : story) {
636 for (Paragraph para : chap) {
637 if (para.getType() == ParagraphType.IMAGE) {
638 size += para.getContentImage().getSize();
639 } else {
640 size += para.getContent().length();
641 }
642 }
6b89e45c
NR
643 }
644
33d40b9f 645 return size;
6b89e45c
NR
646 }
647
3fbc084c
NR
648 public static void main(String[] args) throws IOException {
649 Instance.init();
650 WebLibraryServer web = new WebLibraryServer(false);
651 web.run();
652 }
f433d153 653}