org.akubraproject.fs.FSBlob.java Source code

Java tutorial

Introduction

Here is the source code for org.akubraproject.fs.FSBlob.java

Source

/* $HeadURL$
 * $Id$
 *
 * Copyright (c) 2009-2010 DuraSpace
 * http://duraspace.org
 *
 * In collaboration with Topaz Inc.
 * http://www.topazproject.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.akubraproject.fs;

import org.akubraproject.Blob;
import org.akubraproject.DuplicateBlobException;
import org.akubraproject.MissingBlobException;
import org.akubraproject.UnsupportedIdException;
import org.akubraproject.impl.AbstractBlob;
import org.akubraproject.impl.StreamManager;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.FileChannel;
import java.util.Map;
import java.util.Set;

/**
 * Filesystem-backed Blob implementation.
 *
 * <p>A note on syncing: in order for a newly created, deleted, or moved file to be properly
 * sync'd the directory has to be fsync'd too; however, Java does not provide a way to do this.
 * Hence it is possible to loose a complete file despite having sync'd.
 *
 * @author Chris Wilper
 */
class FSBlob extends AbstractBlob {
    private static final Logger log = LoggerFactory.getLogger(FSBlob.class);

    /**
     * Filesystem blob hint indicating that the {@link #moveTo(URI, Map)} method should perform
     * a safe copy-and-delete to move the file blob from one location to another;
     * associated value must be "true" (case insensitive).
     */
    public static final String FORCE_MOVE_AS_COPY_AND_DELETE = "org.akubraproject.force_move_as_copy_and_delete";

    static final String scheme = "file";
    private final URI canonicalId;
    private final File file;
    private final StreamManager manager;
    private final Set<File> modified;

    /**
     * Create a file based blob
     *
     * @param connection the blob store connection
     * @param baseDir the baseDir of the store
     * @param blobId the identifier for the blob
     * @param manager the stream manager
     * @param modified the set of modified files in the connection; may be null
     * @throws UnsupportedIdException if the given id is not supported
     */
    FSBlob(FSBlobStoreConnection connection, File baseDir, URI blobId, StreamManager manager, Set<File> modified)
            throws UnsupportedIdException {
        super(connection, blobId);
        this.canonicalId = validateId(blobId);
        this.file = new File(baseDir, canonicalId.getRawSchemeSpecificPart());
        this.manager = manager;
        this.modified = modified;
    }

    @Override
    public URI getCanonicalId() {
        return canonicalId;
    }

    @Override
    public InputStream openInputStream() throws IOException {
        ensureOpen();

        if (!file.exists())
            throw new MissingBlobException(getId());

        return manager.manageInputStream(getConnection(), new FileInputStream(file));
    }

    @Override
    public OutputStream openOutputStream(long estimatedSize, boolean overwrite) throws IOException {
        ensureOpen();

        if (!overwrite && file.exists())
            throw new DuplicateBlobException(getId());

        makeParentDirs(file);

        if (modified != null)
            modified.add(file);

        return manager.manageOutputStream(getConnection(), new FileOutputStream(file));
    }

    @Override
    public long getSize() throws IOException {
        ensureOpen();

        if (!file.exists())
            throw new MissingBlobException(getId());

        return file.length();
    }

    @Override
    public boolean exists() throws IOException {
        ensureOpen();

        return file.exists();
    }

    @Override
    public void delete() throws IOException {
        ensureOpen();

        if (!file.delete() && file.exists())
            throw new IOException("Failed to delete file: " + file);

        if (modified != null)
            modified.remove(file);
    }

    /**
     * Move a file-based blob object from one location to another
     *
     * @param blobId The ID of the new (destination) blob
     * @param hints A set of hints for moveTo and getBlob
     * @return The newly-created (destination) blob
     * @throws DuplicateBlobException if destination file already exists
     * @throws IOException on failure to move the source blob to the destination blob
     * @throws MissingBlobException if source file does not exist
     * @see #FORCE_MOVE_AS_COPY_AND_DELETE
     */
    @Override
    public Blob moveTo(URI blobId, Map<String, String> hints) throws IOException {
        boolean force_move = false;

        ensureOpen();
        FSBlob dest = (FSBlob) getConnection().getBlob(blobId, hints);

        File other = dest.file;

        if (other.exists())
            throw new DuplicateBlobException(blobId);

        makeParentDirs(other);

        if (hints != null)
            force_move = Boolean.parseBoolean(hints.get(FORCE_MOVE_AS_COPY_AND_DELETE));

        if (force_move || !file.renameTo(other)) {
            if (!file.exists())
                throw new MissingBlobException(getId());

            boolean success = false;
            try {
                nioCopy(file, other);

                if (file.length() != other.length()) {
                    throw new IOException("Source and destination file sizes do not match: source '" + file
                            + "' is " + file.length() + " and destination '" + other + "' is " + other.length());
                }

                if (!file.delete() && file.exists())
                    throw new IOException("Failed to delete file: " + file);

                success = true;
            } finally {
                if (!success && other.exists() && !other.delete()) {
                    log.error("Error deleting destination file '" + other + "' after source file '" + file
                            + "' copy failure");
                }
            }
        }

        if (modified != null && modified.remove(file))
            modified.add(other);

        return dest;
    }

    static URI validateId(URI blobId) throws UnsupportedIdException {
        if (blobId == null)
            throw new NullPointerException("Id cannot be null");
        if (!blobId.getScheme().equalsIgnoreCase(scheme))
            throw new UnsupportedIdException(blobId, "Id must be in " + scheme + " scheme");
        String path = blobId.getRawSchemeSpecificPart();
        if (path.startsWith("/"))
            throw new UnsupportedIdException(blobId, "Id must specify a relative path");
        try {
            // insert a '/' so java.net.URI normalization works
            URI tmp = new URI(scheme + ":/" + path);
            String nPath = tmp.normalize().getRawSchemeSpecificPart().substring(1);
            if (nPath.equals("..") || nPath.startsWith("../"))
                throw new UnsupportedIdException(blobId, "Id cannot be outside top-level directory");
            if (nPath.endsWith("/"))
                throw new UnsupportedIdException(blobId, "Id cannot specify a directory");
            return new URI(scheme + ":" + nPath);
        } catch (URISyntaxException wontHappen) {
            throw new Error(wontHappen);
        }
    }

    private void makeParentDirs(File file) throws IOException {
        File parent = file.getParentFile();

        if (parent != null && !parent.exists()) {
            parent.mkdirs(); // See https://jira.duraspace.org/browse/AKUBRA-3
            if (!parent.exists())
                throw new IOException("Unable to create parent directory: " + parent.getPath());
        }
    }

    private static void nioCopy(File source, File dest) throws IOException {
        FileInputStream f_in = null;
        FileOutputStream f_out = null;

        log.debug("Performing force copy-and-delete of source '" + source + "' to '" + dest + "'");
        try {
            f_in = new FileInputStream(source);

            try {
                f_out = new FileOutputStream(dest);

                FileChannel in = f_in.getChannel();
                FileChannel out = f_out.getChannel();
                in.transferTo(0, source.length(), out);
            } finally {
                IOUtils.closeQuietly(f_out);
            }
        } finally {
            IOUtils.closeQuietly(f_in);
        }

        if (!dest.exists())
            throw new IOException("Failed to copy file to new location: " + dest);
    }
}