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