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