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