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