Fix default remote lib not using cache
[nikiroo-utils.git] / src / be / nikiroo / fanfix / library / LocalLibrary.java
CommitLineData
e42573a0 1package be.nikiroo.fanfix.library;
68e2c6d2
NR
2
3import java.awt.image.BufferedImage;
4import java.io.File;
5import java.io.FileFilter;
14b57448 6import java.io.FileInputStream;
68e2c6d2 7import java.io.IOException;
14b57448 8import java.io.InputStream;
68e2c6d2
NR
9import java.util.ArrayList;
10import java.util.HashMap;
11import java.util.List;
12import java.util.Map;
13
14b57448
NR
14import javax.imageio.ImageIO;
15
e42573a0 16import be.nikiroo.fanfix.Instance;
68e2c6d2
NR
17import be.nikiroo.fanfix.bundles.Config;
18import be.nikiroo.fanfix.data.MetaData;
19import be.nikiroo.fanfix.data.Story;
20import be.nikiroo.fanfix.output.BasicOutput;
21import be.nikiroo.fanfix.output.BasicOutput.OutputType;
22import be.nikiroo.fanfix.output.InfoCover;
23import be.nikiroo.fanfix.supported.InfoReader;
24import be.nikiroo.utils.IOUtils;
14b57448
NR
25import be.nikiroo.utils.ImageUtils;
26import be.nikiroo.utils.MarkableFileInputStream;
68e2c6d2
NR
27import be.nikiroo.utils.Progress;
28
29/**
30 * This {@link BasicLibrary} will store the stories locally on disk.
31 *
32 * @author niki
33 */
34public class LocalLibrary extends BasicLibrary {
35 private int lastId;
36 private Map<MetaData, File[]> stories; // Files: [ infoFile, TargetFile ]
14b57448 37 private Map<String, BufferedImage> sourceCovers;
68e2c6d2
NR
38
39 private File baseDir;
40 private OutputType text;
41 private OutputType image;
42
e604986c
NR
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
ff05b828
NR
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)
e604986c
NR
67 */
68 public LocalLibrary(File baseDir, String text, String image,
69 boolean defaultIsHtml) {
ff05b828 70 this(baseDir, OutputType.valueOfAllOkUC(text,
e604986c 71 defaultIsHtml ? OutputType.HTML : OutputType.INFO_TEXT),
ff05b828 72 OutputType.valueOfAllOkUC(image,
e604986c
NR
73 defaultIsHtml ? OutputType.HTML : OutputType.CBZ));
74 }
75
68e2c6d2
NR
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
e604986c 82 * the {@link OutputType} to use for non-image documents
68e2c6d2 83 * @param image
e604986c 84 * the {@link OutputType} to use for image documents
68e2c6d2
NR
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;
14b57448 93 this.sourceCovers = new HashMap<String, BufferedImage>();
68e2c6d2
NR
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
ff05b828
NR
104 public File getFile(String luid, Progress pg) {
105 File[] files = getStories(pg).get(getInfo(luid));
68e2c6d2
NR
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) {
62c63b07 125 Instance.getTraceHandler().error(e);
68e2c6d2
NR
126 }
127 }
128 }
129
130 return null;
131 }
132
133 @Override
e272f05f 134 protected void invalidateInfo(String luid) {
68e2c6d2 135 stories = null;
14b57448 136 sourceCovers = new HashMap<String, BufferedImage>();
68e2c6d2
NR
137 }
138
139 @Override
140 protected synchronized int getNextId() {
14b57448 141 getStories(null); // make sure lastId is set
68e2c6d2
NR
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);
e272f05f 150 file.getParentFile().delete();
68e2c6d2
NR
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
925298fd 161 BasicOutput it = BasicOutput.getOutput(getOutputType(meta), true, true);
68e2c6d2
NR
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();
e272f05f 186 relatedFile.getParentFile().delete();
68e2c6d2 187 } catch (IOException e) {
62c63b07 188 Instance.getTraceHandler().error(e);
68e2c6d2
NR
189 }
190 } else {
191 relatedFile.renameTo(new File(newDir, relatedFile.getName()));
e272f05f 192 relatedFile.getParentFile().delete();
68e2c6d2
NR
193 }
194 }
195
e272f05f 196 invalidateInfo();
68e2c6d2
NR
197 }
198
14b57448
NR
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) {
62c63b07 215 Instance.getTraceHandler().error(e);
14b57448
NR
216 sourceCovers.remove(source);
217 }
218 }
219
b89dfb6e
NR
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
ff05b828 227 // Check if we can simply copy the files instead of the whole process
b89dfb6e 228 if (other instanceof LocalLibrary) {
ff05b828 229 LocalLibrary otherLocalLibrary = (LocalLibrary) other;
e604986c 230
e604986c
NR
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());
b89dfb6e 236 File to = this.getExpectedDir(meta.getSource());
e604986c 237 List<File> sources = otherLocalLibrary.getRelatedFiles(luid);
b89dfb6e
NR
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)) {
e604986c 246 target.getParentFile().mkdirs();
b89dfb6e
NR
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
e272f05f 267 invalidateInfo();
b89dfb6e
NR
268 pg.done();
269 return;
270 }
271 }
272
273 super.imprt(other, luid, pg);
e604986c 274
e272f05f 275 invalidateInfo();
b89dfb6e
NR
276 }
277
68e2c6d2
NR
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;
68e2c6d2 289 }
211f7ddb
NR
290
291 return text;
68e2c6d2
NR
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 *
085a2f9a 337 * @param source
68e2c6d2
NR
338 * the type (source)
339 *
340 * @return the target directory
341 */
085a2f9a
NR
342 private File getExpectedDir(String source) {
343 String sanitizedSource = source.replaceAll("[^a-zA-Z0-9._+-]", "_");
344 return new File(baseDir, sanitizedSource);
68e2c6d2
NR
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);
211f7ddb 366 }
68e2c6d2 367
211f7ddb
NR
368 File infoFile = getStories(null).get(meta)[0];
369 File targetFile = getStories(null).get(meta)[1];
68e2c6d2 370
211f7ddb
NR
371 files.add(infoFile);
372 files.add(targetFile);
68e2c6d2 373
211f7ddb
NR
374 String readerExt = getOutputType(meta).getDefaultExtension(true);
375 String fileExt = getOutputType(meta).getDefaultExtension(false);
68e2c6d2 376
211f7ddb
NR
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);
68e2c6d2 382
211f7ddb
NR
383 if (relatedFile.exists()) {
384 files.add(relatedFile);
68e2c6d2 385 }
211f7ddb 386 }
68e2c6d2 387
211f7ddb 388 String coverExt = "."
2a25f781
NR
389 + Instance.getConfig().getString(Config.IMAGE_FORMAT_COVER)
390 .toLowerCase();
211f7ddb
NR
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);
68e2c6d2
NR
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
e272f05f 410 * {@link BasicLibrary#invalidateInfo()}).
68e2c6d2
NR
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() {
211f7ddb 430 @Override
68e2c6d2
NR
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() {
211f7ddb 441 @Override
68e2c6d2
NR
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
14b57448 453 String source = null;
68e2c6d2
NR
454 for (File infoFile : infoFiles) {
455 pgFiles.setName(infoFile.getName());
456 try {
457 MetaData meta = InfoReader.readMeta(infoFile, false);
14b57448 458 source = meta.getSource();
68e2c6d2
NR
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
2a25f781
NR
479 Instance.getTraceHandler().error(
480 new IOException(
481 "Cannot load file from library: "
482 + infoFile, e));
68e2c6d2
NR
483 }
484 pgFiles.add(1);
485 }
486
14b57448
NR
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) {
62c63b07 498 Instance.getTraceHandler().error(e);
14b57448
NR
499 }
500 }
501
68e2c6d2
NR
502 pgFiles.setName(null);
503 }
504
505 pgDirs.setName("Loading directories");
506 }
507
508 return stories;
509 }
085a2f9a
NR
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) {
62c63b07 525 Instance.getTraceHandler().error(e);
085a2f9a
NR
526 sourceCovers.remove(source);
527 }
528 }
68e2c6d2 529}