1 package be
.nikiroo
.fanfix
.library
;
4 import java
.io
.FileFilter
;
5 import java
.io
.FileInputStream
;
6 import java
.io
.FileNotFoundException
;
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
;
14 import be
.nikiroo
.fanfix
.Instance
;
15 import be
.nikiroo
.fanfix
.bundles
.Config
;
16 import be
.nikiroo
.fanfix
.data
.MetaData
;
17 import be
.nikiroo
.fanfix
.data
.Story
;
18 import be
.nikiroo
.fanfix
.output
.BasicOutput
;
19 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
20 import be
.nikiroo
.fanfix
.output
.InfoCover
;
21 import be
.nikiroo
.fanfix
.supported
.InfoReader
;
22 import be
.nikiroo
.utils
.IOUtils
;
23 import be
.nikiroo
.utils
.Image
;
24 import be
.nikiroo
.utils
.Progress
;
27 * This {@link BasicLibrary} will store the stories locally on disk.
31 public class LocalLibrary
extends BasicLibrary
{
33 private Map
<MetaData
, File
[]> stories
; // Files: [ infoFile, TargetFile ]
34 private Map
<String
, Image
> sourceCovers
;
37 private OutputType text
;
38 private OutputType image
;
41 * Create a new {@link LocalLibrary} with the given back-end directory.
44 * the directory where to find the {@link Story} objects
46 public LocalLibrary(File baseDir
) {
47 this(baseDir
, Instance
.getConfig().getString(
48 Config
.NON_IMAGES_DOCUMENT_TYPE
), Instance
.getConfig()
49 .getString(Config
.IMAGES_DOCUMENT_TYPE
), false);
53 * Create a new {@link LocalLibrary} with the given back-end directory.
56 * the directory where to find the {@link Story} objects
58 * the {@link OutputType} to use for non-image documents
60 * the {@link OutputType} to use for image documents
61 * @param defaultIsHtml
62 * if the given text or image is invalid, use HTML by default (if
63 * not, it will be INFO_TEXT/CBZ by default)
65 public LocalLibrary(File baseDir
, String text
, String image
,
66 boolean defaultIsHtml
) {
67 this(baseDir
, OutputType
.valueOfAllOkUC(text
,
68 defaultIsHtml ? OutputType
.HTML
: OutputType
.INFO_TEXT
),
69 OutputType
.valueOfAllOkUC(image
,
70 defaultIsHtml ? OutputType
.HTML
: OutputType
.CBZ
));
74 * Create a new {@link LocalLibrary} with the given back-end directory.
77 * the directory where to find the {@link Story} objects
79 * the {@link OutputType} to use for non-image documents
81 * the {@link OutputType} to use for image documents
83 public LocalLibrary(File baseDir
, OutputType text
, OutputType image
) {
84 this.baseDir
= baseDir
;
90 this.sourceCovers
= null;
96 protected List
<MetaData
> getMetas(Progress pg
) {
97 return new ArrayList
<MetaData
>(getStories(pg
).keySet());
101 public File
getFile(String luid
, Progress pg
) {
102 Instance
.getTraceHandler().trace(
103 this.getClass().getSimpleName() + ": get file for " + luid
);
106 String mess
= "no file found for ";
108 MetaData meta
= getInfo(luid
);
109 File
[] files
= getStories(pg
).get(meta
);
111 mess
= "file retrieved for ";
115 Instance
.getTraceHandler().trace(
116 this.getClass().getSimpleName() + ": " + mess
+ luid
+ " ("
117 + meta
.getTitle() + ")");
123 public Image
getCover(String luid
) {
124 MetaData meta
= getInfo(luid
);
126 if (meta
.getCover() != null) {
127 return meta
.getCover();
130 File
[] files
= getStories(null).get(meta
);
132 File infoFile
= files
[0];
135 meta
= InfoReader
.readMeta(infoFile
, true);
136 return meta
.getCover();
137 } catch (IOException e
) {
138 Instance
.getTraceHandler().error(e
);
147 protected synchronized void updateInfo(MetaData meta
) {
152 protected void invalidateInfo(String luid
) {
158 protected synchronized int getNextId() {
159 getStories(null); // make sure lastId is set
164 protected void doDelete(String luid
) throws IOException
{
165 for (File file
: getRelatedFiles(luid
)) {
166 // TODO: throw an IOException if we cannot delete the files?
167 IOUtils
.deltree(file
);
168 file
.getParentFile().delete();
173 protected Story
doSave(Story story
, Progress pg
) throws IOException
{
174 MetaData meta
= story
.getMeta();
176 File expectedTarget
= getExpectedFile(meta
);
177 expectedTarget
.getParentFile().mkdirs();
179 BasicOutput it
= BasicOutput
.getOutput(getOutputType(meta
), true, true);
180 it
.process(story
, expectedTarget
.getPath(), pg
);
186 protected synchronized void saveMeta(MetaData meta
, Progress pg
)
188 File newDir
= getExpectedDir(meta
.getSource());
189 if (!newDir
.exists()) {
193 List
<File
> relatedFiles
= getRelatedFiles(meta
.getLuid());
194 for (File relatedFile
: relatedFiles
) {
195 // TODO: this is not safe at all.
196 // We should copy all the files THEN delete them
197 // Maybe also adding some rollback cleanup if possible
198 if (relatedFile
.getName().endsWith(".info")) {
200 String name
= relatedFile
.getName().replaceFirst(
202 relatedFile
.delete();
203 InfoCover
.writeInfo(newDir
, name
, meta
);
204 relatedFile
.getParentFile().delete();
205 } catch (IOException e
) {
206 Instance
.getTraceHandler().error(e
);
209 relatedFile
.renameTo(new File(newDir
, relatedFile
.getName()));
210 relatedFile
.getParentFile().delete();
218 public synchronized Image
getCustomSourceCover(String source
) {
219 if (sourceCovers
== null) {
220 sourceCovers
= new HashMap
<String
, Image
>();
223 Image img
= sourceCovers
.get(source
);
228 File coverDir
= getExpectedDir(source
);
229 if (coverDir
.isDirectory()) {
230 File cover
= new File(coverDir
, ".cover.png");
231 if (cover
.exists()) {
234 in
= new FileInputStream(cover
);
236 sourceCovers
.put(source
, new Image(in
));
240 } catch (FileNotFoundException e
) {
242 } catch (IOException e
) {
243 Instance
.getTraceHandler().error(
245 "Cannot load the existing custom source cover: "
251 return sourceCovers
.get(source
);
255 public void setSourceCover(String source
, String luid
) {
256 setSourceCover(source
, getCover(luid
));
260 * Fix the source cover to the given story cover.
263 * the source to change
267 synchronized void setSourceCover(String source
, Image coverImage
) {
268 File dir
= getExpectedDir(source
);
270 File cover
= new File(dir
, ".cover");
272 Instance
.getCache().saveAsImage(coverImage
, cover
, true);
273 if (sourceCovers
!= null) {
274 sourceCovers
.put(source
, coverImage
);
276 } catch (IOException e
) {
277 Instance
.getTraceHandler().error(e
);
282 public void imprt(BasicLibrary other
, String luid
, Progress pg
)
288 // Check if we can simply copy the files instead of the whole process
289 if (other
instanceof LocalLibrary
) {
290 LocalLibrary otherLocalLibrary
= (LocalLibrary
) other
;
292 MetaData meta
= otherLocalLibrary
.getInfo(luid
);
293 String expectedType
= ""
294 + (meta
!= null && meta
.isImageDocument() ? image
: text
);
295 if (meta
!= null && meta
.getType().equals(expectedType
)) {
296 File from
= otherLocalLibrary
.getExpectedDir(meta
.getSource());
297 File to
= this.getExpectedDir(meta
.getSource());
298 List
<File
> relatedFiles
= otherLocalLibrary
299 .getRelatedFiles(luid
);
300 if (!relatedFiles
.isEmpty()) {
301 pg
.setMinMax(0, relatedFiles
.size());
304 for (File relatedFile
: relatedFiles
) {
305 File target
= new File(relatedFile
.getAbsolutePath()
306 .replace(from
.getAbsolutePath(),
307 to
.getAbsolutePath()));
308 if (!relatedFile
.equals(target
)) {
309 target
.getParentFile().mkdirs();
310 InputStream in
= null;
312 in
= new FileInputStream(relatedFile
);
313 IOUtils
.write(in
, target
);
314 } catch (IOException e
) {
318 } catch (Exception ee
) {
336 super.imprt(other
, luid
, pg
);
340 * Return the {@link OutputType} for this {@link Story}.
343 * the {@link Story} {@link MetaData}
347 private OutputType
getOutputType(MetaData meta
) {
348 if (meta
!= null && meta
.isImageDocument()) {
356 * Get the target {@link File} related to the given <tt>.info</tt>
357 * {@link File} and {@link MetaData}.
362 * the <tt>.info</tt> {@link File}
364 * @return the target {@link File}
366 private File
getTargetFile(MetaData meta
, File infoFile
) {
367 // Replace .info with whatever is needed:
368 String path
= infoFile
.getPath();
369 path
= path
.substring(0, path
.length() - ".info".length());
370 String newExt
= getOutputType(meta
).getDefaultExtension(true);
372 return new File(path
+ newExt
);
376 * The target (full path) where the {@link Story} related to this
377 * {@link MetaData} should be located on disk for a new {@link Story}.
380 * the {@link Story} {@link MetaData}
384 private File
getExpectedFile(MetaData key
) {
385 String title
= key
.getTitle();
389 title
= title
.replaceAll("[^a-zA-Z0-9._+-]", "_");
390 return new File(getExpectedDir(key
.getSource()), key
.getLuid() + "_"
395 * The directory (full path) where the new {@link Story} related to this
396 * {@link MetaData} should be located on disk.
401 * @return the target directory
403 private File
getExpectedDir(String source
) {
404 String sanitizedSource
= source
.replaceAll("[^a-zA-Z0-9._+/-]", "_");
406 while (sanitizedSource
.startsWith("/")) {
407 if (sanitizedSource
.length() > 1) {
408 sanitizedSource
= sanitizedSource
.substring(1);
410 sanitizedSource
= "";
414 sanitizedSource
= sanitizedSource
.replace("/", File
.separator
);
416 if (sanitizedSource
.isEmpty()) {
417 sanitizedSource
= "EMPTY";
420 return new File(baseDir
, sanitizedSource
);
424 * Return the list of files/directories on disk for this {@link Story}.
426 * If the {@link Story} is not found, and empty list is returned.
429 * the {@link Story} LUID
431 * @return the list of {@link File}s
433 * @throws IOException
434 * if the {@link Story} was not found
436 private List
<File
> getRelatedFiles(String luid
) throws IOException
{
437 List
<File
> files
= new ArrayList
<File
>();
439 MetaData meta
= getInfo(luid
);
441 throw new IOException("Story not found: " + luid
);
444 File infoFile
= getStories(null).get(meta
)[0];
445 File targetFile
= getStories(null).get(meta
)[1];
448 files
.add(targetFile
);
450 String readerExt
= getOutputType(meta
).getDefaultExtension(true);
451 String fileExt
= getOutputType(meta
).getDefaultExtension(false);
453 String path
= targetFile
.getAbsolutePath();
454 if (readerExt
!= null && !readerExt
.equals(fileExt
)) {
455 path
= path
.substring(0, path
.length() - readerExt
.length())
457 File relatedFile
= new File(path
);
459 if (relatedFile
.exists()) {
460 files
.add(relatedFile
);
464 String coverExt
= "."
465 + Instance
.getConfig().getString(Config
.IMAGE_FORMAT_COVER
)
467 File coverFile
= new File(path
+ coverExt
);
468 if (!coverFile
.exists()) {
469 coverFile
= new File(path
.substring(0,
470 path
.length() - fileExt
.length())
474 if (coverFile
.exists()) {
475 files
.add(coverFile
);
482 * Fill the list of stories by reading the content of the local directory
483 * {@link LocalLibrary#baseDir}.
485 * Will use a cached list when possible (see
486 * {@link BasicLibrary#invalidateInfo()}).
489 * the optional {@link Progress}
491 * @return the list of stories (for each item, the first {@link File} is the
492 * info file, the second file is the target {@link File})
494 private synchronized Map
<MetaData
, File
[]> getStories(Progress pg
) {
498 pg
.setMinMax(0, 100);
501 if (stories
== null) {
502 stories
= new HashMap
<MetaData
, File
[]>();
506 File
[] dirs
= baseDir
.listFiles(new FileFilter() {
508 public boolean accept(File file
) {
509 return file
!= null && file
.isDirectory();
514 Progress pgDirs
= new Progress(0, 100 * dirs
.length
);
515 pg
.addProgress(pgDirs
, 100);
517 for (File dir
: dirs
) {
518 Progress pgFiles
= new Progress();
519 pgDirs
.addProgress(pgFiles
, 100);
520 pgDirs
.setName("Loading from: " + dir
.getName());
522 addToStories(pgFiles
, dir
);
524 pgFiles
.setName(null);
527 pgDirs
.setName("Loading directories");
535 private void addToStories(Progress pgFiles
, File dir
) {
536 File
[] infoFilesAndSubdirs
= dir
.listFiles(new FileFilter() {
538 public boolean accept(File file
) {
539 boolean info
= file
!= null && file
.isFile()
540 && file
.getPath().toLowerCase().endsWith(".info");
541 boolean dir
= file
!= null && file
.isDirectory();
546 if (pgFiles
!= null) {
547 pgFiles
.setMinMax(0, infoFilesAndSubdirs
.length
);
550 for (File infoFileOrSubdir
: infoFilesAndSubdirs
) {
551 if (pgFiles
!= null) {
552 pgFiles
.setName(infoFileOrSubdir
.getName());
555 if (infoFileOrSubdir
.isDirectory()) {
556 addToStories(null, infoFileOrSubdir
);
559 MetaData meta
= InfoReader
560 .readMeta(infoFileOrSubdir
, false);
562 int id
= Integer
.parseInt(meta
.getLuid());
567 stories
.put(meta
, new File
[] { infoFileOrSubdir
,
568 getTargetFile(meta
, infoFileOrSubdir
) });
569 } catch (Exception e
) {
571 throw new IOException("Cannot understand the LUID of "
572 + infoFileOrSubdir
+ ": " + meta
.getLuid(), e
);
574 } catch (IOException e
) {
575 // We should not have not-supported files in the
577 Instance
.getTraceHandler().error(
578 new IOException("Cannot load file from library: "
579 + infoFileOrSubdir
, e
));
583 if (pgFiles
!= null) {