@Override
  public short unlink(final long parentId, final String fileName, final AtomicDBUpdate update)
      throws DatabaseException {

    try {

      // retrieve the file metadata
      BufferBackedFileMetadata file = BabuDBStorageHelper.getMetadata(database, parentId, fileName);

      // determine and set the new link count
      short newLinkCount = (short) (file.getLinkCount() - 1);
      file.setLinkCount(newLinkCount);

      // if there will be links remaining after the deletion, update the
      // link count; it must be in the FILE_ID_INDEX, because there have
      // been at least two links
      if (newLinkCount > 0)
        update.addUpdate(
            FILE_ID_INDEX,
            BabuDBStorageHelper.createFileIdIndexKey(file.getId(), FileMetadata.RC_METADATA),
            file.getRCMetadata().getValue());

      // remove all entries from the file index
      update.addUpdate(
          FILE_INDEX,
          BabuDBStorageHelper.createFileKey(parentId, fileName, FileMetadata.FC_METADATA),
          null);
      update.addUpdate(
          FILE_INDEX,
          BabuDBStorageHelper.createFileKey(parentId, fileName, FileMetadata.RC_METADATA),
          null);

      return newLinkCount;

    } catch (BabuDBException exc) {
      throw new DatabaseException(exc);
    }
  }
  @Override
  public void link(
      final FileMetadata metadata,
      final long newParentId,
      final String newFileName,
      final AtomicDBUpdate update) {

    // get the link source
    BufferBackedFileMetadata md = (BufferBackedFileMetadata) metadata;

    // increment the link count
    short links = metadata.getLinkCount();
    md.setLinkCount((short) (links + 1));

    // insert the whole metadata of the original file in the file ID
    // index
    update.addUpdate(
        FILE_ID_INDEX,
        BabuDBStorageHelper.createFileIdIndexKey(metadata.getId(), FileMetadata.FC_METADATA),
        md.getFCMetadataValue());
    update.addUpdate(
        FILE_ID_INDEX,
        BabuDBStorageHelper.createFileIdIndexKey(metadata.getId(), FileMetadata.RC_METADATA),
        md.getRCMetadata().getValue());

    // remove the back link
    update.addUpdate(
        FILE_ID_INDEX, BabuDBStorageHelper.createFileIdIndexKey(metadata.getId(), (byte) 3), null);

    // if the metadata was retrieved from the FILE_INDEX and hasn't
    // been deleted before (i.e. links == 0), ensure that the original
    // file in the file index now points to the file ID index, and
    // remove the FC and XLoc metadata entries
    if (links != 0 && md.getIndexId() == FILE_INDEX) {

      update.addUpdate(
          FILE_INDEX,
          md.getRCMetadata().getKey(),
          BabuDBStorageHelper.createLinkTarget(metadata.getId()));
      update.addUpdate(FILE_INDEX, md.getFCMetadataKey(), null);
    }

    // create an entry for the new link to the metadata in the file
    // index
    update.addUpdate(
        FILE_INDEX,
        BabuDBStorageHelper.createFileKey(newParentId, newFileName, FileMetadata.RC_METADATA),
        BabuDBStorageHelper.createLinkTarget(metadata.getId()));
  }
  @Override
  public FileMetadata getMetadata(long fileId) throws DatabaseException {

    try {

      // create the key for the file ID index lookup
      byte[] key = BabuDBStorageHelper.createFileIdIndexKey(fileId, (byte) -1);
      ByteBuffer.wrap(key).putLong(fileId);

      byte[][] valBufs = new byte[BufferBackedFileMetadata.NUM_BUFFERS][];

      // retrieve the metadata from the link index
      ResultSet<byte[], byte[]> it =
          database.prefixLookup(BabuDBStorageManager.FILE_ID_INDEX, key, null).get();

      while (it.hasNext()) {

        Entry<byte[], byte[]> curr = it.next();

        int type = BabuDBStorageHelper.getType(curr.getKey(), BabuDBStorageManager.FILE_ID_INDEX);

        // if the value is a back link, resolve it
        if (type == 3) {

          long parentId = ByteBuffer.wrap(curr.getValue()).getLong();
          String fileName = new String(curr.getValue(), 8, curr.getValue().length - 8);

          return getMetadata(parentId, fileName);
        }

        valBufs[type] = curr.getValue();
      }

      it.free();

      // if not metadata was found for the file ID, return null
      if (valBufs[FileMetadata.RC_METADATA] == null) return null;

      byte[][] keyBufs =
          new byte[][] {null, BabuDBStorageHelper.createFileKey(0, "", FileMetadata.RC_METADATA)};

      // otherwise, a hard link target is contained in the index; create a
      // new metadata object in this case
      return new BufferBackedFileMetadata(keyBufs, valBufs, BabuDBStorageManager.FILE_ID_INDEX);

    } catch (BabuDBException exc) {
      throw new DatabaseException(exc);
    }
  }
  @Override
  public short delete(final long parentId, final String fileName, final AtomicDBUpdate update)
      throws DatabaseException {

    try {

      // retrieve the file metadata
      BufferBackedFileMetadata file = BabuDBStorageHelper.getMetadata(database, parentId, fileName);

      // check whether there is only one link remaining
      short newLinkCount = (short) (file.getLinkCount() - 1);
      assert (newLinkCount >= 0);

      // decrement the link count
      file.setLinkCount(newLinkCount);

      // if there will be links remaining after the deletion, update the
      // link count
      if (newLinkCount > 0)
        update.addUpdate(
            FILE_ID_INDEX,
            BabuDBStorageHelper.createFileIdIndexKey(file.getId(), FileMetadata.RC_METADATA),
            file.getRCMetadata().getValue());

      // delete all keys ...

      // remove all content from the file index
      update.addUpdate(
          BabuDBStorageManager.FILE_INDEX,
          BabuDBStorageHelper.createFileKey(parentId, fileName, FileMetadata.FC_METADATA),
          null);
      update.addUpdate(
          BabuDBStorageManager.FILE_INDEX,
          BabuDBStorageHelper.createFileKey(parentId, fileName, FileMetadata.RC_METADATA),
          null);

      // if the last link to the file is supposed to be deleted, remove
      // the remaining metadata, including ACLs and XAttrs
      if (newLinkCount == 0) {

        // remove the back link from the file ID index
        update.addUpdate(
            BabuDBStorageManager.FILE_ID_INDEX,
            BabuDBStorageHelper.createFileIdIndexKey(file.getId(), (byte) 3),
            null);

        // remove potentially existing metadata from the file ID index
        update.addUpdate(
            BabuDBStorageManager.FILE_ID_INDEX,
            BabuDBStorageHelper.createFileIdIndexKey(file.getId(), FileMetadata.FC_METADATA),
            null);
        update.addUpdate(
            BabuDBStorageManager.FILE_ID_INDEX,
            BabuDBStorageHelper.createFileIdIndexKey(file.getId(), FileMetadata.RC_METADATA),
            null);

        byte[] idBytes = new byte[8];
        ByteBuffer.wrap(idBytes).putLong(file.getId());

        // remove all ACLs
        ResultSet<byte[], byte[]> it =
            database.prefixLookup(BabuDBStorageManager.ACL_INDEX, idBytes, null).get();
        while (it.hasNext())
          update.addUpdate(BabuDBStorageManager.ACL_INDEX, it.next().getKey(), null);
        it.free();

        // remove all extended attributes
        it = database.prefixLookup(BabuDBStorageManager.XATTRS_INDEX, idBytes, null).get();
        while (it.hasNext())
          update.addUpdate(BabuDBStorageManager.XATTRS_INDEX, it.next().getKey(), null);
        it.free();

        // if a file is deleted, update file count and volume size
        if (file.isDirectory()) {
          updateCount(NUM_DIRS_KEY, false, update);
        } else if (file.getXLocList() != null) {
          volume.updateVolumeSize(-file.getSize(), update);
          updateCount(NUM_FILES_KEY, false, update);
        }
      }

      return file.getLinkCount();

    } catch (BabuDBException exc) {
      throw new DatabaseException(exc);
    }
  }
  @Override
  public void createSnapshot(String snapName, long parentId, String dirName, boolean recursive)
      throws DatabaseException {

    try {

      // determine the prefixes for the snapshot
      byte[][][] prefixes = null;

      FileMetadata snapDir = getMetadata(parentId, dirName);

      // for a full volume snapshot, simply use a 'null' prefix (full:
      // dirID == 1 && recursive)
      if (snapDir.getId() != 1 || !recursive) {

        // get the IDs of all files and directories contained in the
        // given directory; if recursive == true, include subdirectories
        List<FileMetadata> nestedFiles = new LinkedList<FileMetadata>();
        BabuDBStorageHelper.getNestedFiles(nestedFiles, database, snapDir.getId(), recursive);

        List<byte[]> dirEntryPrefixes = new ArrayList<byte[]>(nestedFiles.size());
        List<byte[]> filePrefixes = new ArrayList<byte[]>(nestedFiles.size());

        // include the extended attributes of the volume's root
        // directory if it's not the snapshot directory - they are
        // needed to access volume-wide parameters in the snapshot, such
        // as the access control policy
        if (snapDir.getId() != 1) filePrefixes.add(ByteBuffer.wrap(new byte[8]).putLong(1).array());

        // include all metadata of the snapshot (i.e. top level) dir
        byte[] idxKey = BabuDBStorageHelper.createFileKey(parentId, dirName, (byte) -1);
        byte[] fileKey = BabuDBStorageHelper.createFilePrefixKey(snapDir.getId());
        dirEntryPrefixes.add(idxKey);
        filePrefixes.add(fileKey);

        // include the snapshot directory content
        idxKey = BabuDBStorageHelper.createFilePrefixKey(snapDir.getId());
        dirEntryPrefixes.add(idxKey);

        // determine the key prefixes of all nested files to include and
        // exclude
        for (FileMetadata file : nestedFiles) {

          // create a prefix key for the nested file
          byte[] key = BabuDBStorageHelper.createFilePrefixKey(file.getId());

          // if the nested file is a directory, ...
          if (file.isDirectory()) {

            // include the directory in the file prefixes
            // and the directory prefix in the dir entry prefixes
            filePrefixes.add(key);
            dirEntryPrefixes.add(key);
          }

          // if the nested file is a file, ...
          else filePrefixes.add(key);
        }

        byte[][] dirEntryPrefixesA = dirEntryPrefixes.toArray(new byte[dirEntryPrefixes.size()][]);
        byte[][] filePrefixesA = filePrefixes.toArray(new byte[filePrefixes.size()][]);

        Arrays.sort(dirEntryPrefixesA, DefaultByteRangeComparator.getInstance());
        Arrays.sort(filePrefixesA, DefaultByteRangeComparator.getInstance());

        // FILE_INDEX, XATTRS_INDEX, ACL_INDEX, FILE_ID_INDEX,
        // VOLUME_INDEX
        prefixes =
            new byte[][][] {dirEntryPrefixesA, filePrefixesA, filePrefixesA, filePrefixesA, null};
      }

      // create the snapshot
      SnapshotConfig snap = new DefaultSnapshotConfig(snapName, ALL_INDICES, prefixes, null);
      snapMan.createPersistentSnapshot(database.getName(), snap);

    } catch (BabuDBException exc) {
      throw new DatabaseException(exc);
    }
  }