X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;ds=sidebyside;f=src%2Fbe%2Fnikiroo%2Ffanfix%2FCache.java;fp=src%2Fbe%2Fnikiroo%2Ffanfix%2FCache.java;h=75a0f5d52e2c194a8d7b614b005387c38a088824;hb=08fe2e33007063e30fe22dc1d290f8afaa18eb1d;hp=0000000000000000000000000000000000000000;hpb=ed48062ebfb0d611b74834e313bfb0a2b81416e6;p=fanfix.git diff --git a/src/be/nikiroo/fanfix/Cache.java b/src/be/nikiroo/fanfix/Cache.java new file mode 100644 index 0000000..75a0f5d --- /dev/null +++ b/src/be/nikiroo/fanfix/Cache.java @@ -0,0 +1,427 @@ +package be.nikiroo.fanfix; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.HttpURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.FileAlreadyExistsException; +import java.util.Date; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +import javax.imageio.ImageIO; + +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.supported.BasicSupport; +import be.nikiroo.utils.IOUtils; +import be.nikiroo.utils.MarkableFileInputStream; +import be.nikiroo.utils.StringUtils; + +/** + * This cache will manage Internet (and local) downloads, as well as put the + * downloaded files into a cache. + *
+ * As long the cached resource is not too old, it will use it instead of + * retrieving the file again. + * + * @author niki + */ +public class Cache { + private File dir; + private String UA; + private long tooOldChanging; + private long tooOldStable; + private CookieManager cookies; + + /** + * Create a new {@link Cache} object. + * + * @param dir + * the directory to use as cache + * @param UA + * the User-Agent to use to download the resources + * @param hoursChanging + * the number of hours after which a cached file that is thought + * to change ~often is considered too old (or -1 for + * "never too old") + * @param hoursStable + * the number of hours after which a LARGE cached file that is + * thought to change rarely is considered too old (or -1 for + * "never too old") + * + * @throws IOException + * in case of I/O error + */ + public Cache(File dir, String UA, int hoursChanging, int hoursStable) + throws IOException { + this.dir = dir; + this.UA = UA; + this.tooOldChanging = 1000 * 60 * 60 * hoursChanging; + this.tooOldStable = 1000 * 60 * 60 * hoursStable; + + if (dir != null) { + if (!dir.exists()) { + dir.mkdirs(); + } + } + + if (dir == null || !dir.exists()) { + throw new IOException("Cannot create the cache directory: " + + (dir == null ? "null" : dir.getAbsolutePath())); + } + + cookies = new CookieManager(); + cookies.setCookiePolicy(CookiePolicy.ACCEPT_ALL); + CookieHandler.setDefault(cookies); + } + + /** + * Open a resource (will load it from the cache if possible, or save it into + * the cache after downloading if not). + * + * @param url + * the resource to open + * @param support + * the support to use to download the resource + * @param stable + * TRUE for more stable resources, FALSE when they often change + * + * @return the opened resource + * + * @throws IOException + * in case of I/O error + */ + public InputStream open(URL url, BasicSupport support, boolean stable) + throws IOException { + return open(url, support, stable, url); + } + + /** + * Open a resource (will load it from the cache if possible, or save it into + * the cache after downloading if not). + *
+ * The cached resource will be assimilated to the given original {@link URL} + * + * @param url + * the resource to open + * @param support + * the support to use to download the resource + * @param stable + * TRUE for more stable resources, FALSE when they often change + * @param originalUrl + * the original {@link URL} used to locate the cached resource + * + * @return the opened resource + * + * @throws IOException + * in case of I/O error + */ + public InputStream open(URL url, BasicSupport support, boolean stable, + URL originalUrl) throws IOException { + try { + InputStream in = load(originalUrl, false, stable); + if (in == null) { + try { + save(url, support, originalUrl); + } catch (IOException e) { + throw new IOException("Cannot save the url: " + + (url == null ? "null" : url.toString()), e); + } + + in = load(originalUrl, true, stable); + } + + return in; + } catch (IOException e) { + throw new IOException("Cannot open the url: " + + (url == null ? "null" : url.toString()), e); + } + } + + /** + * Refresh the resource into cache if needed. + * + * @param url + * the resource to open + * @param support + * the support to use to download the resource + * @param stable + * TRUE for more stable resources, FALSE when they often change + * + * @return TRUE if it was pre-downloaded + * + * @throws IOException + * in case of I/O error + */ + public void refresh(URL url, BasicSupport support, boolean stable) + throws IOException { + File cached = getCached(url); + if (cached.exists() && !isOld(cached, stable)) { + return; + } + + open(url, support, stable).close(); + } + + /** + * Check the resource to see if it is in the cache. + * + * @param url + * the resource to check + * + * @return TRUE if it is + * + */ + public boolean check(URL url) { + return getCached(url).exists(); + } + + /** + * Open a resource (will load it from the cache if possible, or save it into + * the cache after downloading if not) as an Image, then save it where + * requested. + *
+ * This version will not always work properly if the original file was not
+ * downloaded before.
+ *
+ * @param url
+ * the resource to open
+ *
+ * @return the opened resource image
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public void saveAsImage(URL url, File target) throws IOException {
+ URL cachedUrl = new URL(url.toString()
+ + "."
+ + Instance.getConfig().getString(Config.IMAGE_FORMAT_CONTENT)
+ .toLowerCase());
+ File cached = getCached(cachedUrl);
+
+ if (!cached.exists() || isOld(cached, true)) {
+ InputStream imageIn = Instance.getCache().open(url, null, true);
+ ImageIO.write(StringUtils.toImage(imageIn), Instance.getConfig()
+ .getString(Config.IMAGE_FORMAT_CONTENT).toLowerCase(),
+ cached);
+ }
+
+ IOUtils.write(new FileInputStream(cached), target);
+ }
+
+ /**
+ * Manually add this item to the cache.
+ *
+ * @param in
+ * the input data
+ * @param uniqueID
+ * a unique ID for this resource
+ *
+ * @return the resulting {@link FileAlreadyExistsException}
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public File addToCache(InputStream in, String uniqueID) throws IOException {
+ File file = getCached(new File(uniqueID).toURI().toURL());
+ IOUtils.write(in, file);
+ return file;
+ }
+
+ /**
+ * Clean the cache (delete the cached items).
+ *
+ * @param onlyOld
+ * only clean the files that are considered too old
+ *
+ * @return the number of cleaned items
+ */
+ public int cleanCache(boolean onlyOld) {
+ int num = 0;
+ for (File file : dir.listFiles()) {
+ if (!onlyOld || isOld(file, true)) {
+ if (file.delete()) {
+ num++;
+ } else {
+ System.err.println("Cannot delete temporary file: "
+ + file.getAbsolutePath());
+ }
+ }
+ }
+ return num;
+ }
+
+ /**
+ * Open a resource from the cache if it exists.
+ *
+ * @param url
+ * the resource to open
+ * @return the opened resource
+ * @throws IOException
+ * in case of I/O error
+ */
+ private InputStream load(URL url, boolean allowOld, boolean stable)
+ throws IOException {
+ File cached = getCached(url);
+ if (cached.exists() && !isOld(cached, stable)) {
+ return new MarkableFileInputStream(new FileInputStream(cached));
+ }
+
+ return null;
+ }
+
+ /**
+ * Save the given resource to the cache.
+ *
+ * @param url
+ * the resource
+ * @param support
+ * the {@link BasicSupport} used to download it
+ * @param originalUrl
+ * the original {@link URL} used to locate the cached resource
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws URISyntaxException
+ */
+ private void save(URL url, BasicSupport support, URL originalUrl)
+ throws IOException {
+ URLConnection conn = url.openConnection();
+
+ conn.setRequestProperty("User-Agent", UA);
+ conn.setRequestProperty("Cookie", generateCookies(support));
+ conn.setRequestProperty("Accept-Encoding", "gzip");
+ if (support != null) {
+ conn.setRequestProperty("Referer", support.getCurrentReferer()
+ .toString());
+ conn.setRequestProperty("Host", support.getCurrentReferer()
+ .getHost());
+ }
+
+ conn.connect();
+
+ // Check if redirect
+ if (conn instanceof HttpURLConnection
+ && ((HttpURLConnection) conn).getResponseCode() / 100 == 3) {
+ String newUrl = conn.getHeaderField("Location");
+ save(new URL(newUrl), support, originalUrl);
+ return;
+ }
+
+ InputStream in = conn.getInputStream();
+ if ("gzip".equals(conn.getContentEncoding())) {
+ in = new GZIPInputStream(in);
+ }
+
+ try {
+ File cached = getCached(originalUrl);
+ BufferedOutputStream out = new BufferedOutputStream(
+ new FileOutputStream(cached));
+ try {
+ byte[] buf = new byte[4096];
+ int len;
+ while ((len = in.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ } finally {
+ out.close();
+ }
+ } finally {
+ in.close();
+ }
+ }
+
+ /**
+ * Check if the {@link File} is too old according to
+ * {@link Cache#tooOldChanging}.
+ *
+ * @param file
+ * the file to check
+ * @param stable
+ * TRUE to denote files that are not supposed to change too often
+ *
+ * @return TRUE if it is
+ */
+ private boolean isOld(File file, boolean stable) {
+ long max = tooOldChanging;
+ if (stable) {
+ max = tooOldStable;
+ }
+
+ if (max < 0) {
+ return false;
+ }
+
+ long time = new Date().getTime() - file.lastModified();
+ if (time < 0) {
+ System.err.println("Timestamp in the future for file: "
+ + file.getAbsolutePath());
+ }
+
+ return time < 0 || time > max;
+ }
+
+ /**
+ * Get the cache resource from the cache if it is present for this
+ * {@link URL}.
+ *
+ * @param url
+ * the url
+ * @return the cached version if present, NULL if not
+ */
+ private File getCached(URL url) {
+ String name = url.getHost();
+ if (name == null || name.length() == 0) {
+ name = url.getFile();
+ } else {
+ name = url.toString();
+ }
+
+ name = name.replace('/', '_').replace(':', '_');
+
+ return new File(dir, name);
+ }
+
+ /**
+ * Generate the cookie {@link String} from the local {@link CookieStore} so
+ * it is ready to be passed.
+ *
+ * @return the cookie
+ */
+ private String generateCookies(BasicSupport support) {
+ StringBuilder builder = new StringBuilder();
+ for (HttpCookie cookie : cookies.getCookieStore().getCookies()) {
+ if (builder.length() > 0) {
+ builder.append(';');
+ }
+
+ // TODO: check if format is ok
+ builder.append(cookie.toString());
+ }
+
+ if (support != null) {
+ for (Map.Entry