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