Copying files with file locks in Java

3.7k views Asked by At

I'm working on a Java process that should efficiently (and recursively) copy files/directories from a source location to a destination location.

To do this, I want to:

  • create a lock
  • if the destination file does not exist, copy it
  • else if the destination file is different, copy it
  • else they are the same, so do nothing
  • release the lock

To check the contents are equal, I was planning on using the Apache Commons IO FileUtils method contentsEqual(...). To do the copying, I was planning to use the Apache Commons IO FileUtils method copyFile(...).

So, the code I came up with is (this is just for files, directories are handled recursively down to this method for files):

private static void checkAndUpdateFile(File src, File dest) throws IOException {
  FileOutputStream out = new FileOutputStream(dest);
  FileChannel channel = out.getChannel();
  FileLock lock = channel.lock();

  if (!dest.exists()) {
    FileUtils.copyFile(src, out);
  } else if (!FileUtils.contentEquals(src, dest)) {
    FileUtils.copyFile(src, out);
  } 

  lock.release();
  channel.close();
  out.close();
}

This does the file locking (great), and copies the files (super).

However, whenever it copies the files, it sets the copied files' last modified timestamp to the time of the copy. This means that subsequent calls to FileUtils.contentEquals(src, dest) continue to return false, so the files are re-copied.

What I'd really like is similar to FileUtils.copyFile(src, dest, true), which preserves the file timestamps - and does get true from calling FileUtils.contentEquals(src, dest). That would require the lock to be on the File, not the FileOutputStream, as otherwise the call to FileUtils.copyFile(src, dest, true) fails and throws an Exception because the file is locked.

Alternatively, I considered doing what the FileUtils.copyFile(src, dest, true) method does, which is to call dest.setLastModified(src.lastModified()). However, this would have to be called after the lock is released, which could cause problems if the same process has been executed more than once simultaneously.

I had also considered the ides of putting the lock on the source file, but that doesn't help as I'd have to put it on a FileInputStream, and I want to pass a File to FileUtils.copyFile(src, dest).

So:

  1. Is there any easier way to achieve what I am trying to do?
  2. Is it possible to put a lock on a File, rather than a derivative of File?
  3. What would be the best way of resolving this?
    • i.e. do I just need to write my own approach method for part of this? (probably copyFile(...))
2

There are 2 answers

0
amaidment On BEST ANSWER

So... in the end, I went for the approach of writing my own copy() and compare() methods to use the FileChannel objects which have been locked. Below is the solution I came up with - although I expect others might be able to suggest improvements. This was informed by looking at the source code for apache.commons.io classes FileUtils and IOUtils.

private static final int s_eof = -1;
private static final int s_byteBuffer = 10240;

private static void checkAndUpdateFile(File src, File dest) throws IOException {
  FileInputStream in = new FileInputStream(src);
  FileChannel srcChannel = in.getChannel();
  FileChannel destChannel = null;
  FileLock destLock = null;

  try {
    if (!dest.exists()) {
      final RandomAccessFile destFile = new RandomAccessFile(dest, "rw");
      destChannel = destFile.getChannel();
      destLock = destChannel.lock();
      copyFileChannels(srcChannel, destChannel);
      dest.setLastModified(src.lastModified());
    } else {
      final RandomAccessFile destFile = new RandomAccessFile(dest, "rw");
      destChannel = destFile.getChannel();
      destLock = destChannel.lock();
      if (!compareFileChannels(srcChannel, destChannel)) {
        copyFileChannels(srcChannel, destChannel);
        dest.setLastModified(src.lastModified());
      }
    }
  } finally {
    if (destLock != null) {
      destLock.release();
    }
    if (destChannel != null) {
      destChannel.close();
    }
    srcChannel.close();
    in.close();
  }
}

protected static void copyFileChannels(FileChannel src, 
                                       FileChannel dest) throws IOException {
  final long size = src.size();
  for (long pos = 0; pos < size; ) {
    long count = 
      ((size - pos) > s_byteBuffer) ? s_byteBuffer : (size - pos);
    pos += dest.transferFrom(src, pos, count);
  }
}

protected static boolean compareFileChannels(FileChannel a, 
                                             FileChannel b) throws IOException {
  if (a.size() != b.size()) {
    return false;
  } else {
    final ByteBuffer aBuffer = ByteBuffer.allocate(s_byteBuffer);
    final ByteBuffer bBuffer = ByteBuffer.allocate(s_byteBuffer);
    for (int aCh = a.read(aBuffer); s_eof != aCh; ) {
      int bCh = b.read(bBuffer);
      if (aCh != bCh || aBuffer.compareTo(bBuffer) != 0) {
        return false;
      }
      aBuffer.clear();
      aCh = a.read(aBuffer);
      bBuffer.clear();
    }
    return s_eof == b.read(bBuffer);
  }
}
4
Makky On

You can use Guava Google Core Library to compare two files.

As Byte Source

ByteSource Doc

        ByteSource inByte = Resources.asByteSource(srcFileURL);
        ByteSource outByte = Files.asByteSource(srcFileURL2);
        boolean fileEquals= inByte.contentEquals(outByte));