/** * eAdventure (formerly <e-Adventure> and <e-Game>) is a research project of the * <e-UCM> research group. * * Copyright 2005-2010 <e-UCM> research group. * * You can access a list of all the contributors to eAdventure at: * http://e-adventure.e-ucm.es/contributors * * <e-UCM> is a research group of the Department of Software Engineering * and Artificial Intelligence at the Complutense University of Madrid * (School of Computer Science). * * C Profesor Jose Garcia Santesmases sn, * 28040 Madrid (Madrid), Spain. * * For more info please visit: <http://e-adventure.e-ucm.es> or * <http://www.e-ucm.es> * * **************************************************************************** * * This file is part of eAdventure, version 2.0 * * eAdventure is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * eAdventure is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with eAdventure. If not, see <http://www.gnu.org/licenses/>. */ package es.eucm.ead.editor.control.commands; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.security.MessageDigest; import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import es.eucm.ead.tools.java.utils.FileUtils; /** * A cache intended to allow undo/redo for file operations. The cache is backed * by a 'base' folder, into which file hashes (with the corresponding contents) * are saved. Key management is entirely up to the user. Out-of-application * changes can be checked for using key.sameAsFor(f,deep). * * @author mfreire */ public class FileCache { private static Logger logger = LoggerFactory.getLogger(FileCache.class); /** * Base directory for file access; allows safe relative filenames. */ private File base; public FileCache(File base) { this.base = base; if (!base.isDirectory() || base.mkdir()) { throw new IllegalArgumentException( "Could not create file cache directory"); } } /** * Shows bytes, as series of two-digit hex chars. * @param b * @return the bytes, encoded in hex. */ private static String showBytes(byte[] b) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < b.length; i++) { sb.append(String.format("%02x", b[i])); } return sb.toString(); } /** * Saves the file for later retrieval. * @param f file to save (can be null; an empty file is then saved) * @return the key from which to retrieve file-contents later. * Lose the key, lose the file. * @throws IOException */ public Key saveFile(File f) throws IOException { Key k = new Key(); k.addAttributes(f); k.addContents(f); File dest = new File(base, showBytes(k.hash)); if (!dest.exists()) { if (f == null) { dest.createNewFile(); } else { FileUtils.copy(f, dest); } } return k; } /** * Restores a saved file. * @param key returned when saving the file * @param dest File where the recovered bytes should be placed. * @throws IOException */ public void restoreFile(Key key, File dest) throws IOException { File source = new File(base, showBytes(key.hash)); FileUtils.copy(source, dest); } /** * Used to find files in the cache. */ public static class Key { private static MessageDigest digester = null; private String[] attributes; private byte[] hash; public void addAttributes(File f) throws IOException { if (attributes == null) { attributes = computeAttributes(f); } } public void addContents(File f) throws IOException { if (hash == null) { hash = computeContentsHash(f); } } private String dump() { return this + " a: " + Arrays.asList(attributes) + (hash != null ? " c10: " + showBytes(hash) : "no-hash"); } public boolean sameAsFor(File f, boolean deep) throws IOException { if (logger.isInfoEnabled()) { logger.info("this: " + dump()); } Key k = new Key(); k.addAttributes(f); if (logger.isInfoEnabled()) { logger.info("other1: " + k.dump()); } if (Arrays.deepEquals(attributes, k.attributes)) { k.addContents(f); if (logger.isInfoEnabled()) { logger.info("other2: " + k.dump()); } // FIXME: 'deep' is being ignored return Arrays.equals(hash, k.hash); } return false; } /** * Returns a hash for a file's meta-data. Reads file size, * creation & modification time (but not file-contents) into a string. * Different strings implies * "different" files (different attributes; but contents *may* be the same) */ private static String[] computeAttributes(File f) throws IOException { return new String[] { f.getPath(), "" + f.length(), "" + f.lastModified() }; } /** * Returns a hash for a file's contents. Reads file contents into a hashing * function. * @param f a file * @return the file's hash * @throws IOException */ private static byte[] computeContentsHash(File f) throws IOException { FileChannel fc = null; try { if (digester == null) { digester = MessageDigest.getInstance("SHA-1"); } ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); if (f != null && f.exists() && f.canRead()) { fc = new RandomAccessFile(f, "r").getChannel(); while (fc.position() < fc.size()) { fc.read(buffer); buffer.flip(); digester.update(buffer.array(), 0, buffer.limit()); buffer.clear(); } } else { logger.info("no such file {} - using empty contents", f); } byte[] output = digester.digest(); digester.reset(); buffer.clear(); if (fc != null) { fc.close(); fc = null; } return output; } catch (Throwable e) { throw new IOException("Error hashing" + f + "; see log", e); } finally { try { if (fc != null) { fc.close(); } } catch (Exception e) { logger.error("Error closing file-channel after hashing {}", f, e); } } } } }