de.cosmocode.palava.store.FileSystemStore.java Source code

Java tutorial

Introduction

Here is the source code for de.cosmocode.palava.store.FileSystemStore.java

Source

/**
 * Copyright 2010 CosmoCode GmbH
 *
 * 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 de.cosmocode.palava.store;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.util.Collection;
import java.util.Set;
import java.util.UUID;

import javax.annotation.Nullable;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Collections2;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.google.common.io.InputSupplier;
import com.google.inject.Inject;
import com.google.inject.name.Named;

/**
 * File system based implementation of the {@link Store} interface.
 *
 * <p>
 *   This implementation uses {@link UUID} to generate unique identifiers.
 *   {@link InputStream}s will be saved as files in sub-directories.
 *   Each sub directory represents on segment of the generated uuid, splitted
 *   by the dash signs.
 * </p>
 *
 * @author Willi Schoenborn
 */
public final class FileSystemStore extends AbstractByteStore {

    private static final Logger LOG = LoggerFactory.getLogger(FileSystemStore.class);

    private final File directory;

    private IdGenerator generator = new UUIDGenerator();

    private FileIdentifier fileIdentifier = new DefaultFileIdentifier();

    private final Function<File, String> toIdentifier = new Function<File, String>() {

        @Override
        public String apply(File from) {
            return fileIdentifier.toIdentifier(directory, from);
        }

    };

    private String unixOwner;

    private String unixPermissions;

    @Inject
    FileSystemStore(@Named(FileSystemStoreConfig.DIRECTORY) File directory) throws IOException {
        Preconditions.checkNotNull(directory, "Directory");
        FileUtils.forceMkdir(directory);
        this.directory = directory;
    }

    @Inject(optional = true)
    void setGenerator(@Named(StoreConfig.ID_GENERATOR) IdGenerator generator) {
        this.generator = Preconditions.checkNotNull(generator, "Generator");
    }

    @Inject(optional = true)
    void setFileIdentifier(@Named(FileSystemStoreConfig.FILE_IDENTIFIER) FileIdentifier identifier) {
        this.fileIdentifier = identifier;
    }

    @Inject(optional = true)
    public void setUnixOwner(@Named(FileSystemStoreConfig.UNIX_OWNER) @Nullable String unixOwner) {
        this.unixOwner = unixOwner;
    }

    @Inject(optional = true)
    public void setUnixPermissions(
            @Named(FileSystemStoreConfig.UNIX_PERMISSIONS) @Nullable String unixPermissions) {
        this.unixPermissions = unixPermissions;
    }

    public FileIdentifier getFileIdentifier() {
        return fileIdentifier;
    }

    @Override
    public String create(InputStream stream) throws IOException {
        Preconditions.checkNotNull(stream, "Stream");
        final String uuid = generator.generate();
        create(stream, uuid);
        return uuid;
    }

    @Override
    public void create(final InputStream stream, String identifier) throws IOException {
        Preconditions.checkNotNull(stream, "Stream");
        final File file = getFile(identifier);
        Preconditions.checkState(!file.exists(), "File %s is already present", file);
        LOG.trace("Storing {} to {}", stream, file);

        Files.createParentDirs(file);
        Files.copy(asSupplier(stream), file);

        final Process chown = setOwner(file);
        final Process chmod = setPermissions(file);

        try {
            waitAndCheck(chown);
            waitAndCheck(chmod);
        } catch (InterruptedException e) {
            throw new IOException(e);
        } finally {
            close(chown);
            close(chmod);
        }
    }

    private void waitAndCheck(Process process) throws InterruptedException, IOException {
        if (process.waitFor() == 0) {
            return;
        } else {
            final byte[] bytes = ByteStreams.toByteArray(process.getErrorStream());
            final String message = new String(bytes, Charsets.UTF_8);
            throw new IOException(message);
        }
    }

    private void close(Process process) {
        Closeables.closeQuietly(process.getInputStream());
        Closeables.closeQuietly(process.getOutputStream());
        Closeables.closeQuietly(process.getErrorStream());
    }

    private Process setOwner(File file) throws IOException {
        if (unixOwner == null) {
            LOG.trace("No unix owner configured, using defaults");
            return DummyProcess.getInstance();
        } else {
            final String path = file.getAbsolutePath();
            LOG.trace("Setting unix owner to {} for file {}", unixOwner, path);
            return exec("chown %s %s", unixOwner, path);
        }
    }

    private Process setPermissions(File file) throws IOException {
        if (unixPermissions == null) {
            LOG.trace("No unix permissions configured, using defaults");
            return DummyProcess.getInstance();
        } else {
            final String path = file.getAbsolutePath();
            LOG.trace("Setting unix permissions to {} for file {}", unixPermissions, path);
            return exec("chmod %s %s", unixPermissions, path);
        }
    }

    private Process exec(String template, Object... arguments) throws IOException {
        final String command = String.format(template, arguments);
        LOG.trace("Executing '{}'", command);
        return Runtime.getRuntime().exec(command);
    }

    private InputSupplier<InputStream> asSupplier(final InputStream stream) {
        return new InputSupplier<InputStream>() {

            @Override
            public InputStream getInput() throws IOException {
                return stream;
            }

        };
    }

    @Override
    public ByteBuffer view(String identifier) throws IOException {
        Preconditions.checkNotNull(identifier, "Identifier");
        final File file = getFile(identifier);
        Preconditions.checkState(file.exists(), "%s does not exist", file);
        LOG.trace("Reading file from {}", file);
        final FileChannel channel = new RandomAccessFile(file, "r").getChannel();
        return channel.map(MapMode.READ_ONLY, 0, channel.size());
    }

    @Override
    public Set<String> list() throws IOException {
        final IOFileFilter fileFilter = FileFilterUtils.fileFileFilter();
        final IOFileFilter directoryFilter = TrueFileFilter.INSTANCE;
        @SuppressWarnings("unchecked")
        final Collection<File> files = FileUtils.listFiles(directory, fileFilter, directoryFilter);
        return Sets.newHashSet(Collections2.transform(files, toIdentifier));
    }

    /**
     * {@inheritDoc}
     *
     * <p>
     *   Recursively deletes empty parent directories.
     * </p>
     */
    @Override
    public void delete(String identifier) throws IOException {
        Preconditions.checkNotNull(identifier, "Identifier");
        final File file = getFile(identifier);
        Preconditions.checkState(file.exists(), "%s does not exist", file);
        LOG.trace("Removing {} from store", file);
        FileUtils.forceDelete(file);
        deleteEmptyParent(file.getParentFile());
    }

    /**
     * Provides a file pointing to the target as specified by
     * the given identifier.
     *
     * @param identifier the file identifier
     * @return a file (may not exist)
     */
    private File getFile(String identifier) {
        return fileIdentifier.toFile(directory, identifier);
    }

    /**
     * Reads a file from this store.
     *
     * @param identifier the identifier of the binary data being retrieved
     * @return the file associated with the given identifier
     * @throws IOException if file does not exist
     */
    public File readFile(String identifier) throws IOException {
        Preconditions.checkNotNull(identifier, "Identifier");
        final File file = getFile(identifier);
        if (file.exists()) {
            return file;
        } else {
            throw new FileNotFoundException(file.getAbsolutePath());
        }
    }

    private void deleteEmptyParent(File file) throws IOException {
        Preconditions.checkArgument(file.isDirectory(), "%s has to be a directory", file);

        // do not delete configured directory
        if (directory.equals(file))
            return;

        if (file.list().length > 0) {
            LOG.trace("Keeping non empty directory {}", file);
            return;
        }

        LOG.trace("Deleting empty directory {}", file);
        FileUtils.deleteDirectory(file);
        deleteEmptyParent(file.getParentFile());
    }

}