/* * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, * Version 1.0, and under the Eclipse Public License, Version 1.0 * (http://h2database.com/html/license.html). * Initial Developer: H2 Group */ package org.h2.store; import java.io.IOException; import java.lang.ref.Reference; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import org.h2.constant.ErrorCode; import org.h2.constant.SysProperties; import org.h2.engine.Constants; import org.h2.message.DbException; import org.h2.security.SecureFileStore; import org.h2.store.fs.FileUtils; import org.h2.util.TempFileDeleter; import org.h2.util.Utils; /** * This class is an abstraction of a random access file. * Each file contains a magic header, and reading / writing is done in blocks. * See also {@link SecureFileStore} */ public class FileStore { /** * The size of the file header in bytes. */ public static final int HEADER_LENGTH = 3 * Constants.FILE_BLOCK_SIZE; /** * The magic file header. */ private static final String HEADER = "-- H2 0.5/B -- ".substring(0, Constants.FILE_BLOCK_SIZE - 1) + "\n"; /** * The file name. */ protected String name; /** * The callback object is responsible to check access rights, and free up * disk space if required. */ protected DataHandler handler; private FileChannel file; private long filePos; private long fileLength; private Reference<?> autoDeleteReference; private boolean checkedWriting = true; private String mode; private TempFileDeleter tempFileDeleter; private boolean textMode; private java.nio.channels.FileLock lock; /** * Create a new file using the given settings. * * @param handler the callback object * @param name the file name * @param mode the access mode ("r", "rw", "rws", "rwd") */ protected FileStore(DataHandler handler, String name, String mode) { this.handler = handler; this.name = name; this.mode = mode; if (handler != null) { tempFileDeleter = handler.getTempFileDeleter(); } try { boolean exists = FileUtils.exists(name); if (exists && !FileUtils.canWrite(name)) { mode = "r"; this.mode = mode; } else { FileUtils.createDirectories(FileUtils.getParent(name)); } file = FileUtils.open(name, mode); if (exists) { fileLength = file.size(); } } catch (IOException e) { throw DbException.convertIOException(e, "name: " + name + " mode: " + mode); } } /** * Open a non encrypted file store with the given settings. * * @param handler the data handler * @param name the file name * @param mode the access mode (r, rw, rws, rwd) * @return the created object */ public static FileStore open(DataHandler handler, String name, String mode) { return open(handler, name, mode, null, null, 0); } /** * Open an encrypted file store with the given settings. * * @param handler the data handler * @param name the file name * @param mode the access mode (r, rw, rws, rwd) * @param cipher the name of the cipher algorithm * @param key the encryption key * @return the created object */ public static FileStore open(DataHandler handler, String name, String mode, String cipher, byte[] key) { return open(handler, name, mode, cipher, key, Constants.ENCRYPTION_KEY_HASH_ITERATIONS); } /** * Open an encrypted file store with the given settings. * * @param handler the data handler * @param name the file name * @param mode the access mode (r, rw, rws, rwd) * @param cipher the name of the cipher algorithm * @param key the encryption key * @param keyIterations the number of iterations the key should be hashed * @return the created object */ public static FileStore open(DataHandler handler, String name, String mode, String cipher, byte[] key, int keyIterations) { FileStore store; if (cipher == null) { store = new FileStore(handler, name, mode); } else { store = new SecureFileStore(handler, name, mode, cipher, key, keyIterations); } return store; } /** * Generate the random salt bytes if required. * * @return the random salt or the magic */ protected byte[] generateSalt() { return HEADER.getBytes(); } /** * Initialize the key using the given salt. * * @param salt the salt */ protected void initKey(byte[] salt) { // do nothing } public void setCheckedWriting(boolean value) { this.checkedWriting = value; } private void checkWritingAllowed() { if (handler != null && checkedWriting) { handler.checkWritingAllowed(); } } private void checkPowerOff() { if (handler != null) { handler.checkPowerOff(); } } /** * Initialize the file. This method will write or check the file header if * required. */ public void init() { int len = Constants.FILE_BLOCK_SIZE; byte[] salt; byte[] magic = HEADER.getBytes(); if (length() < HEADER_LENGTH) { // write unencrypted checkedWriting = false; writeDirect(magic, 0, len); salt = generateSalt(); writeDirect(salt, 0, len); initKey(salt); // write (maybe) encrypted write(magic, 0, len); checkedWriting = true; } else { // read unencrypted seek(0); byte[] buff = new byte[len]; readFullyDirect(buff, 0, len); if (Utils.compareNotNull(buff, magic) != 0) { throw DbException.get(ErrorCode.FILE_VERSION_ERROR_1, name); } salt = new byte[len]; readFullyDirect(salt, 0, len); initKey(salt); // read (maybe) encrypted readFully(buff, 0, Constants.FILE_BLOCK_SIZE); if (textMode) { buff[10] = 'B'; } if (Utils.compareNotNull(buff, magic) != 0) { throw DbException.get(ErrorCode.FILE_ENCRYPTION_ERROR_1, name); } } } /** * Close the file. */ public void close() { if (file != null) { try { trace("close", name, file); file.close(); } catch (IOException e) { throw DbException.convertIOException(e, name); } finally { file = null; } } } /** * Close the file without throwing any exceptions. Exceptions are simply * ignored. */ public void closeSilently() { try { close(); } catch (Exception e) { // ignore } } /** * Close the file (ignoring exceptions) and delete the file. */ public void closeAndDeleteSilently() { if (file != null) { closeSilently(); tempFileDeleter.deleteFile(autoDeleteReference, name); name = null; } } /** * Read a number of bytes without decrypting. * * @param b the target buffer * @param off the offset * @param len the number of bytes to read */ protected void readFullyDirect(byte[] b, int off, int len) { readFully(b, off, len); } /** * Read a number of bytes. * * @param b the target buffer * @param off the offset * @param len the number of bytes to read */ public void readFully(byte[] b, int off, int len) { if (SysProperties.CHECK && (len < 0 || len % Constants.FILE_BLOCK_SIZE != 0)) { DbException.throwInternalError("unaligned read " + name + " len " + len); } checkPowerOff(); try { FileUtils.readFully(file, ByteBuffer.wrap(b, off, len)); } catch (IOException e) { throw DbException.convertIOException(e, name); } filePos += len; } /** * Go to the specified file location. * * @param pos the location */ public void seek(long pos) { if (SysProperties.CHECK && pos % Constants.FILE_BLOCK_SIZE != 0) { DbException.throwInternalError("unaligned seek " + name + " pos " + pos); } try { if (pos != filePos) { file.position(pos); filePos = pos; } } catch (IOException e) { throw DbException.convertIOException(e, name); } } /** * Write a number of bytes without encrypting. * * @param b the source buffer * @param off the offset * @param len the number of bytes to write */ protected void writeDirect(byte[] b, int off, int len) { write(b, off, len); } /** * Write a number of bytes. * * @param b the source buffer * @param off the offset * @param len the number of bytes to write */ public void write(byte[] b, int off, int len) { if (SysProperties.CHECK && (len < 0 || len % Constants.FILE_BLOCK_SIZE != 0)) { DbException.throwInternalError("unaligned write " + name + " len " + len); } checkWritingAllowed(); checkPowerOff(); try { FileUtils.writeFully(file, ByteBuffer.wrap(b, off, len)); } catch (IOException e) { closeFileSilently(); throw DbException.convertIOException(e, name); } filePos += len; fileLength = Math.max(filePos, fileLength); } /** * Set the length of the file. This will expand or shrink the file. * * @param newLength the new file size */ public void setLength(long newLength) { if (SysProperties.CHECK && newLength % Constants.FILE_BLOCK_SIZE != 0) { DbException.throwInternalError("unaligned setLength " + name + " pos " + newLength); } checkPowerOff(); checkWritingAllowed(); try { if (newLength > fileLength) { long pos = filePos; file.position(newLength - 1); FileUtils.writeFully(file, ByteBuffer.wrap(new byte[1])); file.position(pos); } else { file.truncate(newLength); } fileLength = newLength; } catch (IOException e) { closeFileSilently(); throw DbException.convertIOException(e, name); } } /** * Get the file size in bytes. * * @return the file size */ public long length() { try { long len = fileLength; if (SysProperties.CHECK2) { len = file.size(); if (len != fileLength) { DbException.throwInternalError("file " + name + " length " + len + " expected " + fileLength); } } if (SysProperties.CHECK2 && len % Constants.FILE_BLOCK_SIZE != 0) { long newLength = len + Constants.FILE_BLOCK_SIZE - (len % Constants.FILE_BLOCK_SIZE); file.truncate(newLength); fileLength = newLength; DbException.throwInternalError("unaligned file length " + name + " len " + len); } return len; } catch (IOException e) { throw DbException.convertIOException(e, name); } } /** * Get the current location of the file pointer. * * @return the location */ public long getFilePointer() { if (SysProperties.CHECK2) { try { if (file.position() != filePos) { DbException.throwInternalError(); } } catch (IOException e) { throw DbException.convertIOException(e, name); } } return filePos; } /** * Call fsync. Depending on the operating system and hardware, this may or * may not in fact write the changes. */ public void sync() { try { file.force(true); } catch (IOException e) { closeFileSilently(); throw DbException.convertIOException(e, name); } } /** * Automatically delete the file once it is no longer in use. */ public void autoDelete() { if (autoDeleteReference == null) { autoDeleteReference = tempFileDeleter.addFile(name, this); } } /** * No longer automatically delete the file once it is no longer in use. */ public void stopAutoDelete() { tempFileDeleter.stopAutoDelete(autoDeleteReference, name); autoDeleteReference = null; } /** * Close the file. The file may later be re-opened using openFile. */ public void closeFile() throws IOException { file.close(); file = null; } /** * Just close the file, without setting the reference to null. This method * is called when writing failed. The reference is not set to null so that * there are no NullPointerExceptions later on. */ private void closeFileSilently() { try { file.close(); } catch (IOException e) { // ignore } } /** * Re-open the file. The file pointer will be reset to the previous * location. */ public void openFile() throws IOException { if (file == null) { file = FileUtils.open(name, mode); file.position(filePos); } } private static void trace(String method, String fileName, Object o) { if (SysProperties.TRACE_IO) { System.out.println("FileStore." + method + " " + fileName + " " + o); } } /** * Check if the file store is in text mode. * * @return true if it is */ public boolean isTextMode() { return textMode; } /** * Try to lock the file. * * @return true if successful */ public synchronized boolean tryLock() { try { lock = file.tryLock(); return lock != null; } catch (Exception e) { // ignore OverlappingFileLockException return false; } } /** * Release the file lock. */ public synchronized void releaseLock() { if (file != null && lock != null) { try { lock.release(); } catch (Exception e) { // ignore } lock = null; } } }