Fixes:
[nikiroo-utils.git] / src / be / nikiroo / fanfix / library / LocalLibrary.java
CommitLineData
e42573a0 1package be.nikiroo.fanfix.library;
68e2c6d2 2
68e2c6d2
NR
3import java.io.File;
4import java.io.FileFilter;
14b57448 5import java.io.FileInputStream;
68e2c6d2 6import java.io.IOException;
14b57448 7import java.io.InputStream;
68e2c6d2
NR
8import java.util.ArrayList;
9import java.util.HashMap;
10import java.util.List;
11import java.util.Map;
12
e42573a0 13import be.nikiroo.fanfix.Instance;
68e2c6d2
NR
14import be.nikiroo.fanfix.bundles.Config;
15import be.nikiroo.fanfix.data.MetaData;
16import be.nikiroo.fanfix.data.Story;
17import be.nikiroo.fanfix.output.BasicOutput;
18import be.nikiroo.fanfix.output.BasicOutput.OutputType;
19import be.nikiroo.fanfix.output.InfoCover;
20import be.nikiroo.fanfix.supported.InfoReader;
21import be.nikiroo.utils.IOUtils;
16a81ef7 22import be.nikiroo.utils.Image;
68e2c6d2
NR
23import be.nikiroo.utils.Progress;
24
25/**
26 * This {@link BasicLibrary} will store the stories locally on disk.
27 *
28 * @author niki
29 */
30public class LocalLibrary extends BasicLibrary {
31 private int lastId;
32 private Map<MetaData, File[]> stories; // Files: [ infoFile, TargetFile ]
16a81ef7 33 private Map<String, Image> sourceCovers;
68e2c6d2
NR
34
35 private File baseDir;
36 private OutputType text;
37 private OutputType image;
38
e604986c
NR
39 /**
40 * Create a new {@link LocalLibrary} with the given back-end directory.
41 *
42 * @param baseDir
43 * the directory where to find the {@link Story} objects
44 */
45 public LocalLibrary(File baseDir) {
46 this(baseDir, Instance.getConfig().getString(
47 Config.NON_IMAGES_DOCUMENT_TYPE), Instance.getConfig()
48 .getString(Config.IMAGES_DOCUMENT_TYPE), false);
49 }
50
51 /**
52 * Create a new {@link LocalLibrary} with the given back-end directory.
53 *
54 * @param baseDir
55 * the directory where to find the {@link Story} objects
ff05b828
NR
56 * @param text
57 * the {@link OutputType} to use for non-image documents
58 * @param image
59 * the {@link OutputType} to use for image documents
60 * @param defaultIsHtml
61 * if the given text or image is invalid, use HTML by default (if
62 * not, it will be INFO_TEXT/CBZ by default)
e604986c
NR
63 */
64 public LocalLibrary(File baseDir, String text, String image,
65 boolean defaultIsHtml) {
ff05b828 66 this(baseDir, OutputType.valueOfAllOkUC(text,
e604986c 67 defaultIsHtml ? OutputType.HTML : OutputType.INFO_TEXT),
ff05b828 68 OutputType.valueOfAllOkUC(image,
e604986c
NR
69 defaultIsHtml ? OutputType.HTML : OutputType.CBZ));
70 }
71
68e2c6d2
NR
72 /**
73 * Create a new {@link LocalLibrary} with the given back-end directory.
74 *
75 * @param baseDir
76 * the directory where to find the {@link Story} objects
77 * @param text
e604986c 78 * the {@link OutputType} to use for non-image documents
68e2c6d2 79 * @param image
e604986c 80 * the {@link OutputType} to use for image documents
68e2c6d2
NR
81 */
82 public LocalLibrary(File baseDir, OutputType text, OutputType image) {
83 this.baseDir = baseDir;
84 this.text = text;
85 this.image = image;
86
87 this.lastId = 0;
88 this.stories = null;
b56c9d60 89 this.sourceCovers = null;
68e2c6d2
NR
90
91 baseDir.mkdirs();
92 }
93
94 @Override
95 protected List<MetaData> getMetas(Progress pg) {
96 return new ArrayList<MetaData>(getStories(pg).keySet());
97 }
98
99 @Override
ff05b828 100 public File getFile(String luid, Progress pg) {
9e2fad36
NR
101 Instance.getTraceHandler().trace(
102 this.getClass().getSimpleName() + ": get file for " + luid);
103
104 File file = null;
105 String mess = "no file found for ";
106
107 MetaData meta = getInfo(luid);
108 Instance.getTraceHandler().trace("(info is: " + meta + ")");
109
110 File[] files = getStories(pg).get(meta);
68e2c6d2 111 if (files != null) {
9e2fad36
NR
112 mess = "file retrieved for ";
113 file = files[1];
68e2c6d2
NR
114 }
115
9e2fad36
NR
116 Instance.getTraceHandler().trace(
117 this.getClass().getSimpleName() + ": " + mess + luid);
118
119 return file;
68e2c6d2
NR
120 }
121
122 @Override
16a81ef7 123 public Image getCover(String luid) {
68e2c6d2
NR
124 MetaData meta = getInfo(luid);
125 if (meta != null) {
126 File[] files = getStories(null).get(meta);
127 if (files != null) {
128 File infoFile = files[0];
129
130 try {
131 meta = InfoReader.readMeta(infoFile, true);
132 return meta.getCover();
133 } catch (IOException e) {
62c63b07 134 Instance.getTraceHandler().error(e);
68e2c6d2
NR
135 }
136 }
137 }
138
139 return null;
140 }
141
142 @Override
efa3c511
NR
143 protected synchronized void updateInfo(MetaData meta) {
144 deleteInfo();
145 }
146
147 @Override
148 protected void deleteInfo(String luid) {
68e2c6d2 149 stories = null;
b56c9d60 150 sourceCovers = null;
68e2c6d2
NR
151 }
152
153 @Override
154 protected synchronized int getNextId() {
14b57448 155 getStories(null); // make sure lastId is set
68e2c6d2
NR
156 return ++lastId;
157 }
158
159 @Override
160 protected void doDelete(String luid) throws IOException {
161 for (File file : getRelatedFiles(luid)) {
162 // TODO: throw an IOException if we cannot delete the files?
163 IOUtils.deltree(file);
e272f05f 164 file.getParentFile().delete();
68e2c6d2
NR
165 }
166 }
167
168 @Override
169 protected Story doSave(Story story, Progress pg) throws IOException {
170 MetaData meta = story.getMeta();
171
172 File expectedTarget = getExpectedFile(meta);
173 expectedTarget.getParentFile().mkdirs();
174
925298fd 175 BasicOutput it = BasicOutput.getOutput(getOutputType(meta), true, true);
68e2c6d2
NR
176 it.process(story, expectedTarget.getPath(), pg);
177
178 return story;
179 }
180
181 @Override
182 protected synchronized void saveMeta(MetaData meta, Progress pg)
183 throws IOException {
184 File newDir = getExpectedDir(meta.getSource());
185 if (!newDir.exists()) {
186 newDir.mkdir();
187 }
188
189 List<File> relatedFiles = getRelatedFiles(meta.getLuid());
190 for (File relatedFile : relatedFiles) {
191 // TODO: this is not safe at all.
192 // We should copy all the files THEN delete them
193 // Maybe also adding some rollback cleanup if possible
194 if (relatedFile.getName().endsWith(".info")) {
195 try {
196 String name = relatedFile.getName().replaceFirst(
197 "\\.info$", "");
198 InfoCover.writeInfo(newDir, name, meta);
199 relatedFile.delete();
e272f05f 200 relatedFile.getParentFile().delete();
68e2c6d2 201 } catch (IOException e) {
62c63b07 202 Instance.getTraceHandler().error(e);
68e2c6d2
NR
203 }
204 } else {
205 relatedFile.renameTo(new File(newDir, relatedFile.getName()));
e272f05f 206 relatedFile.getParentFile().delete();
68e2c6d2
NR
207 }
208 }
209
efa3c511 210 deleteInfo();
68e2c6d2
NR
211 }
212
14b57448 213 @Override
16a81ef7 214 public Image getSourceCover(String source) {
b56c9d60
NR
215 if (sourceCovers == null) {
216 getStories(null);
217 }
218
14b57448
NR
219 if (!sourceCovers.containsKey(source)) {
220 sourceCovers.put(source, super.getSourceCover(source));
221 }
222
223 return sourceCovers.get(source);
224 }
225
226 @Override
227 public void setSourceCover(String source, String luid) {
b56c9d60
NR
228 if (sourceCovers == null) {
229 getStories(null);
230 }
231
14b57448 232 sourceCovers.put(source, getCover(luid));
ecfb936e 233 File cover = new File(getExpectedDir(source), ".cover");
14b57448 234 try {
16a81ef7
NR
235 Instance.getCache().saveAsImage(sourceCovers.get(source), cover,
236 true);
14b57448 237 } catch (IOException e) {
62c63b07 238 Instance.getTraceHandler().error(e);
14b57448
NR
239 sourceCovers.remove(source);
240 }
241 }
242
b89dfb6e
NR
243 @Override
244 public void imprt(BasicLibrary other, String luid, Progress pg)
245 throws IOException {
246 if (pg == null) {
247 pg = new Progress();
248 }
249
ff05b828 250 // Check if we can simply copy the files instead of the whole process
b89dfb6e 251 if (other instanceof LocalLibrary) {
ff05b828 252 LocalLibrary otherLocalLibrary = (LocalLibrary) other;
e604986c 253
e604986c
NR
254 MetaData meta = otherLocalLibrary.getInfo(luid);
255 String expectedType = ""
256 + (meta != null && meta.isImageDocument() ? image : text);
257 if (meta != null && meta.getType().equals(expectedType)) {
258 File from = otherLocalLibrary.getExpectedDir(meta.getSource());
b89dfb6e 259 File to = this.getExpectedDir(meta.getSource());
e604986c 260 List<File> sources = otherLocalLibrary.getRelatedFiles(luid);
b89dfb6e
NR
261 if (!sources.isEmpty()) {
262 pg.setMinMax(0, sources.size());
263 }
264
265 for (File source : sources) {
266 File target = new File(source.getAbsolutePath().replace(
267 from.getAbsolutePath(), to.getAbsolutePath()));
268 if (!source.equals(target)) {
e604986c 269 target.getParentFile().mkdirs();
b89dfb6e
NR
270 InputStream in = null;
271 try {
272 in = new FileInputStream(source);
273 IOUtils.write(in, target);
274 } catch (IOException e) {
275 if (in != null) {
276 try {
277 in.close();
278 } catch (Exception ee) {
279 }
280 }
281
282 pg.done();
283 throw e;
284 }
285 }
286
287 pg.add(1);
288 }
289
efa3c511 290 deleteInfo();
b89dfb6e
NR
291 pg.done();
292 return;
293 }
294 }
295
296 super.imprt(other, luid, pg);
297 }
298
68e2c6d2
NR
299 /**
300 * Return the {@link OutputType} for this {@link Story}.
301 *
302 * @param meta
303 * the {@link Story} {@link MetaData}
304 *
305 * @return the type
306 */
307 private OutputType getOutputType(MetaData meta) {
308 if (meta != null && meta.isImageDocument()) {
309 return image;
68e2c6d2 310 }
211f7ddb
NR
311
312 return text;
68e2c6d2
NR
313 }
314
315 /**
316 * Get the target {@link File} related to the given <tt>.info</tt>
317 * {@link File} and {@link MetaData}.
318 *
319 * @param meta
320 * the meta
321 * @param infoFile
322 * the <tt>.info</tt> {@link File}
323 *
324 * @return the target {@link File}
325 */
326 private File getTargetFile(MetaData meta, File infoFile) {
327 // Replace .info with whatever is needed:
328 String path = infoFile.getPath();
329 path = path.substring(0, path.length() - ".info".length());
330 String newExt = getOutputType(meta).getDefaultExtension(true);
331
332 return new File(path + newExt);
333 }
334
335 /**
336 * The target (full path) where the {@link Story} related to this
337 * {@link MetaData} should be located on disk for a new {@link Story}.
338 *
339 * @param key
340 * the {@link Story} {@link MetaData}
341 *
342 * @return the target
343 */
344 private File getExpectedFile(MetaData key) {
345 String title = key.getTitle();
346 if (title == null) {
347 title = "";
348 }
349 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
350 return new File(getExpectedDir(key.getSource()), key.getLuid() + "_"
351 + title);
352 }
353
354 /**
355 * The directory (full path) where the new {@link Story} related to this
356 * {@link MetaData} should be located on disk.
357 *
085a2f9a 358 * @param source
68e2c6d2
NR
359 * the type (source)
360 *
361 * @return the target directory
362 */
085a2f9a
NR
363 private File getExpectedDir(String source) {
364 String sanitizedSource = source.replaceAll("[^a-zA-Z0-9._+-]", "_");
365 return new File(baseDir, sanitizedSource);
68e2c6d2
NR
366 }
367
368 /**
369 * Return the list of files/directories on disk for this {@link Story}.
370 * <p>
371 * If the {@link Story} is not found, and empty list is returned.
372 *
373 * @param luid
374 * the {@link Story} LUID
375 *
376 * @return the list of {@link File}s
377 *
378 * @throws IOException
379 * if the {@link Story} was not found
380 */
381 private List<File> getRelatedFiles(String luid) throws IOException {
382 List<File> files = new ArrayList<File>();
383
384 MetaData meta = getInfo(luid);
385 if (meta == null) {
386 throw new IOException("Story not found: " + luid);
211f7ddb 387 }
68e2c6d2 388
211f7ddb
NR
389 File infoFile = getStories(null).get(meta)[0];
390 File targetFile = getStories(null).get(meta)[1];
68e2c6d2 391
211f7ddb
NR
392 files.add(infoFile);
393 files.add(targetFile);
68e2c6d2 394
211f7ddb
NR
395 String readerExt = getOutputType(meta).getDefaultExtension(true);
396 String fileExt = getOutputType(meta).getDefaultExtension(false);
68e2c6d2 397
211f7ddb
NR
398 String path = targetFile.getAbsolutePath();
399 if (readerExt != null && !readerExt.equals(fileExt)) {
400 path = path.substring(0, path.length() - readerExt.length())
401 + fileExt;
402 File relatedFile = new File(path);
68e2c6d2 403
211f7ddb
NR
404 if (relatedFile.exists()) {
405 files.add(relatedFile);
68e2c6d2 406 }
211f7ddb 407 }
68e2c6d2 408
211f7ddb 409 String coverExt = "."
2a25f781
NR
410 + Instance.getConfig().getString(Config.IMAGE_FORMAT_COVER)
411 .toLowerCase();
211f7ddb
NR
412 File coverFile = new File(path + coverExt);
413 if (!coverFile.exists()) {
414 coverFile = new File(path.substring(0,
415 path.length() - fileExt.length())
416 + coverExt);
417 }
418
419 if (coverFile.exists()) {
420 files.add(coverFile);
68e2c6d2
NR
421 }
422
423 return files;
424 }
425
426 /**
427 * Fill the list of stories by reading the content of the local directory
428 * {@link LocalLibrary#baseDir}.
429 * <p>
430 * Will use a cached list when possible (see
efa3c511 431 * {@link BasicLibrary#deleteInfo()}).
68e2c6d2
NR
432 *
433 * @param pg
434 * the optional {@link Progress}
435 *
9e2fad36
NR
436 * @return the list of stories (for each item, the first {@link File} is the info file, the
437 * second file is the target {@link File})
68e2c6d2
NR
438 */
439 private synchronized Map<MetaData, File[]> getStories(Progress pg) {
440 if (pg == null) {
441 pg = new Progress();
442 } else {
443 pg.setMinMax(0, 100);
444 }
445
446 if (stories == null) {
447 stories = new HashMap<MetaData, File[]>();
b56c9d60 448 sourceCovers = new HashMap<String, Image>();
68e2c6d2
NR
449
450 lastId = 0;
451
452 File[] dirs = baseDir.listFiles(new FileFilter() {
211f7ddb 453 @Override
68e2c6d2
NR
454 public boolean accept(File file) {
455 return file != null && file.isDirectory();
456 }
457 });
458
b4f9071c
NR
459 if (dirs != null) {
460 Progress pgDirs = new Progress(0, 100 * dirs.length);
461 pg.addProgress(pgDirs, 100);
462
463 for (File dir : dirs) {
464 File[] infoFiles = dir.listFiles(new FileFilter() {
465 @Override
466 public boolean accept(File file) {
467 return file != null
468 && file.getPath().toLowerCase()
469 .endsWith(".info");
470 }
471 });
472
473 Progress pgFiles = new Progress(0, infoFiles.length);
474 pgDirs.addProgress(pgFiles, 100);
475 pgDirs.setName("Loading from: " + dir.getName());
476
477 String source = null;
478 for (File infoFile : infoFiles) {
479 pgFiles.setName(infoFile.getName());
68e2c6d2 480 try {
b4f9071c
NR
481 MetaData meta = InfoReader
482 .readMeta(infoFile, false);
483 source = meta.getSource();
484 try {
485 int id = Integer.parseInt(meta.getLuid());
486 if (id > lastId) {
487 lastId = id;
488 }
68e2c6d2 489
b4f9071c
NR
490 stories.put(meta, new File[] { infoFile,
491 getTargetFile(meta, infoFile) });
492 } catch (Exception e) {
493 // not normal!!
494 throw new IOException(
495 "Cannot understand the LUID of "
fd25eddc
NR
496 + infoFile + ": "
497 + meta.getLuid(), e);
b4f9071c
NR
498 }
499 } catch (IOException e) {
500 // We should not have not-supported files in the
501 // library
502 Instance.getTraceHandler().error(
503 new IOException(
504 "Cannot load file from library: "
505 + infoFile, e));
68e2c6d2 506 }
b4f9071c 507 pgFiles.add(1);
68e2c6d2 508 }
68e2c6d2 509
b4f9071c
NR
510 File cover = new File(dir, ".cover.png");
511 if (cover.exists()) {
14b57448 512 try {
b4f9071c
NR
513 InputStream in = new FileInputStream(cover);
514 try {
515 sourceCovers.put(source, new Image(in));
516 } finally {
517 in.close();
518 }
519 } catch (IOException e) {
520 Instance.getTraceHandler().error(e);
14b57448 521 }
14b57448 522 }
b4f9071c
NR
523
524 pgFiles.setName(null);
14b57448
NR
525 }
526
b4f9071c 527 pgDirs.setName("Loading directories");
68e2c6d2 528 }
68e2c6d2
NR
529 }
530
b4f9071c 531 pg.done();
68e2c6d2
NR
532 return stories;
533 }
085a2f9a
NR
534
535 /**
536 * Fix the source cover to the given story cover.
537 *
538 * @param source
539 * the source to change
540 * @param coverImage
541 * the cover image
542 */
16a81ef7 543 void setSourceCover(String source, Image coverImage) {
b56c9d60
NR
544 if (sourceCovers == null) {
545 getStories(null);
546 }
547
085a2f9a 548 sourceCovers.put(source, coverImage);
ecfb936e 549 File cover = new File(getExpectedDir(source), ".cover");
085a2f9a 550 try {
16a81ef7
NR
551 Instance.getCache().saveAsImage(sourceCovers.get(source), cover,
552 true);
085a2f9a 553 } catch (IOException e) {
62c63b07 554 Instance.getTraceHandler().error(e);
085a2f9a
NR
555 sourceCovers.remove(source);
556 }
557 }
68e2c6d2 558}