/*
* Aphelion
* Copyright (c) 2013 Joris van der Wel
*
* This file is part of Aphelion
*
* Aphelion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* Aphelion 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 General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Aphelion. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, the following supplemental terms apply, based on section 7 of
* the GNU Affero General Public License (version 3):
* a) Preservation of all legal notices and author attributions
* b) Prohibition of misrepresentation of the origin of this material, and
* modified versions are required to be marked in reasonable ways as
* different from the original version (for example by appending a copyright notice).
*
* Linking this library statically or dynamically with other modules is making a
* combined work based on this library. Thus, the terms and conditions of the
* GNU Affero General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your
* choice, provided that you also meet, for each linked independent module,
* the terms and conditions of the license of that module. An independent
* module is a module which is not derived from or based on this library.
*/
package aphelion.shared.resource;
import aphelion.shared.swissarmyknife.SwissArmyKnife;
import aphelion.shared.swissarmyknife.ThreadSafe;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.BitSet;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author Joris
*/
public class AssetCache
{
private static final Logger log = Logger.getLogger("aphelion.resource");
private final FileStorage storage;
public AssetCache(FileStorage storage)
{
this.storage = storage;
}
@ThreadSafe
public FileStorage getStorage()
{
return storage;
}
// 5-bit index
private static final char[] ENCODE_TABLE = {
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'2', '3', '4', '5', '6', '7',
};
@ThreadSafe
private static String encodeFileName(byte[] sha_256, long fileSize)
{
if (sha_256.length != 32)
{
throw new IllegalArgumentException();
}
assert ENCODE_TABLE.length == 32;
ByteBuffer buf = ByteBuffer.allocate(32 + 8);
buf.put(sha_256);
buf.putLong(fileSize);
buf.rewind();
BitSet bits = BitSet.valueOf(buf); //note: little endian
StringBuilder ret = new StringBuilder(64); // (32+8)*8/5
for (int i = 0; i < (32+8)*8; i += 5)
{
int n = 0;
n |= bits.get(i+0) ? 1: 0;
n |= bits.get(i+1) ? 2: 0;
n |= bits.get(i+2) ? 4: 0;
n |= bits.get(i+3) ? 8: 0;
n |= bits.get(i+4) ? 16: 0;
ret.append(ENCODE_TABLE[n]);
}
return ret.toString();
}
@ThreadSafe
public File getAsset(byte[] sha_256, long fileSize)
{
File file = storage.getFile(encodeFileName(sha_256, fileSize));
if (file.exists())
{
if (file.length() != fileSize)
{
// this might happen if a copy or move operation failed halfway through
log.log(Level.SEVERE, "Asset file in cache has been modified or damaged (size mismatch): {0}", file);
file.delete();
return file;
}
// Not checking the hash here (this is slow).
// Use validateAsset() for that
if (!file.setLastModified(System.currentTimeMillis()))
{
log.log(Level.WARNING, "Unable to touch file {0}", file);
}
}
return file;
}
@ThreadSafe
public boolean validateAsset(byte[] sha_256, long fileSize)
{
File file = storage.getFile(encodeFileName(sha_256, fileSize));
if (file.exists())
{
if (file.length() != fileSize)
{
// this might happen if a copy or move operation failed halfway through
log.log(Level.SEVERE, "Asset file in cache has been modified or damaged (size mismatch): {0}", file);
file.delete();
return false;
}
byte[] actual_hash;
try
{
actual_hash = SwissArmyKnife.fileHash("sha-256", file);
if (!Arrays.equals(actual_hash, sha_256))
{
log.log(Level.SEVERE, "Asset file in cache has been modified or damaged (checksum mismatch): {0}", file);
file.delete();
return false;
}
}
catch (IOException ex)
{
// This should not occur (unless the original asset has a length of 0, which is silly),
// length() should have already returned 0
log.log(Level.SEVERE, "", ex);
file.delete();
return false;
}
return true;
}
return false;
}
/**
* @param tmpFile The temp file to be moved or copi
* @param copy false = move tmpFile; true = copy tmpFile
* @param sha_256 The expected hash
* @param fileSize The expected file size
* @throws java.io.IOException The file can not be read
* @throws InvalidContentException The downloaded content is invalid, it does not match the expected hash and size
*/
@ThreadSafe
public void storeAsset(File tmpFile, boolean copy, byte[] sha_256, long fileSize) throws IOException, InvalidContentException
{
byte[] actual_hash = SwissArmyKnife.fileHash("sha-256", tmpFile);
long actual_size = tmpFile.length();
if (!Arrays.equals(actual_hash, sha_256) || fileSize != actual_size)
{
throw new InvalidContentException("The given asset file does not match the expected hash or file size.");
}
File target = storage.getFile(encodeFileName(sha_256, fileSize));
if (copy)
{
try
{
Files.copy(tmpFile.toPath(), target.toPath());
}
catch(FileAlreadyExistsException ex)
{
// do nothing, the files should be identical
}
catch(IOException ex)
{
target.delete();
throw ex;
}
}
else
{
// this method does nothing if the file already exists,
// which is fine since the content would be identical too
try
{
Files.move(tmpFile.toPath(), target.toPath());
}
catch(FileAlreadyExistsException ex)
{
// do nothing, the files should be identical
}
catch(IOException ex)
{
target.delete();
throw ex;
}
}
// These 3 return a boolean to indicate failure, not an exception
// Everyone may read, no one may write
target.setReadOnly();
target.setWritable(false, true);
target.setWritable(false, false);
target.setReadable(true, true);
target.setReadable(true, false);
}
// todo clean up old assets
public static class InvalidContentException extends Exception
{
public InvalidContentException(String message)
{
super(message);
}
}
}