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