org.jclouds.blobstore.config.LocalBlobStore.java Source code

Java tutorial

Introduction

Here is the source code for org.jclouds.blobstore.config.LocalBlobStore.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.jclouds.blobstore.config;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Throwables.getCausalChain;
import static com.google.common.base.Throwables.propagate;
import static com.google.common.collect.Iterables.size;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Iterables.tryFind;
import static com.google.common.collect.Sets.filter;
import static com.google.common.collect.Sets.newTreeSet;
import static org.jclouds.blobstore.options.ListContainerOptions.Builder.recursive;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.UUID;
import java.util.concurrent.ExecutorService;

import javax.annotation.Resource;
import javax.inject.Inject;
import javax.inject.Singleton;

import org.jclouds.blobstore.BlobStore;
import org.jclouds.blobstore.BlobStoreContext;
import org.jclouds.blobstore.ContainerNotFoundException;
import org.jclouds.blobstore.KeyNotFoundException;
import org.jclouds.blobstore.LocalStorageStrategy;
import org.jclouds.blobstore.domain.Blob;
import org.jclouds.blobstore.domain.BlobAccess;
import org.jclouds.blobstore.domain.BlobBuilder;
import org.jclouds.blobstore.domain.BlobBuilder.PayloadBlobBuilder;
import org.jclouds.blobstore.domain.BlobMetadata;
import org.jclouds.blobstore.domain.ContainerAccess;
import org.jclouds.blobstore.domain.MultipartPart;
import org.jclouds.blobstore.domain.MultipartUpload;
import org.jclouds.blobstore.domain.MutableBlobMetadata;
import org.jclouds.blobstore.domain.MutableStorageMetadata;
import org.jclouds.blobstore.domain.PageSet;
import org.jclouds.blobstore.domain.StorageMetadata;
import org.jclouds.blobstore.domain.StorageType;
import org.jclouds.blobstore.domain.internal.MutableStorageMetadataImpl;
import org.jclouds.blobstore.domain.internal.PageSetImpl;
import org.jclouds.blobstore.options.CopyOptions;
import org.jclouds.blobstore.options.CreateContainerOptions;
import org.jclouds.blobstore.options.GetOptions;
import org.jclouds.blobstore.options.ListContainerOptions;
import org.jclouds.blobstore.options.PutOptions;
import org.jclouds.blobstore.util.BlobStoreUtils;
import org.jclouds.blobstore.util.BlobUtils;
import org.jclouds.collect.Memoized;
import org.jclouds.domain.Location;
import org.jclouds.http.HttpCommand;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpResponse;
import org.jclouds.http.HttpResponseException;
import org.jclouds.http.HttpUtils;
import org.jclouds.io.ByteStreams2;
import org.jclouds.io.ContentMetadata;
import org.jclouds.io.ContentMetadataCodec;
import org.jclouds.io.Payload;
import org.jclouds.logging.Logger;
import org.jclouds.util.Closeables2;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.io.ByteSource;
import com.google.common.net.HttpHeaders;

@Singleton
public final class LocalBlobStore implements BlobStore {
    private static final String MULTIPART_PREFIX = ".mpus-";

    @Resource
    private Logger logger = Logger.NULL;

    private final BlobStoreContext context;
    private final BlobUtils blobUtils;
    private final Supplier<Set<? extends Location>> locations;
    private final ContentMetadataCodec contentMetadataCodec;
    private final Blob.Factory blobFactory;
    private final LocalStorageStrategy storageStrategy;

    @Inject
    LocalBlobStore(BlobStoreContext context, BlobUtils blobUtils,
            @Memoized Supplier<Set<? extends Location>> locations, ContentMetadataCodec contentMetadataCodec,
            Blob.Factory blobFactory, LocalStorageStrategy storageStrategy) {
        this.context = checkNotNull(context, "context");
        this.blobUtils = checkNotNull(blobUtils, "blobUtils");
        this.locations = checkNotNull(locations, "locations");
        this.blobFactory = blobFactory;
        this.contentMetadataCodec = contentMetadataCodec;
        this.storageStrategy = storageStrategy;
    }

    @Override
    public BlobStoreContext getContext() {
        return context;
    }

    @Override
    public BlobBuilder blobBuilder(String name) {
        return blobUtils.blobBuilder().name(name);
    }

    /** This implementation invokes {@link #list(String, ListContainerOptions)} */
    @Override
    public PageSet<? extends StorageMetadata> list(String containerName) {
        return this.list(containerName, ListContainerOptions.NONE);
    }

    /**
     * This implementation invokes {@link #countBlobs} with the
     * {@link ListContainerOptions#recursive} option.
     */
    @Override
    public long countBlobs(String containerName) {
        return countBlobs(containerName, recursive());
    }

    /**
     * This implementation invokes {@link BlobUtils#countBlobs}
     */
    @Override
    public long countBlobs(final String containerName, final ListContainerOptions options) {
        return blobUtils.countBlobs(containerName, options);
    }

    /**
     * This implementation invokes {@link #clearContainer} with the
     * {@link ListContainerOptions#recursive} option.
     */
    @Override
    public void clearContainer(String containerName) {
        clearContainer(containerName, recursive());
    }

    @Override
    public void clearContainer(String containerName, ListContainerOptions options) {
        blobUtils.clearContainer(containerName, options);
    }

    @Override
    public void deleteDirectory(final String containerName, final String directory) {
        blobUtils.deleteDirectory(containerName, directory);
    }

    @Override
    public boolean directoryExists(String containerName, String directory) {
        return blobUtils.directoryExists(containerName, directory);
    }

    @Override
    public void createDirectory(String containerName, String directory) {
        if (!blobUtils.directoryExists(containerName, directory)) {
            blobUtils.createDirectory(containerName, directory);
        }
    }

    /**
     * This implementation invokes {@link #getBlob(String,String, GetOptions)}
     */
    @Override
    public Blob getBlob(String containerName, String key) {
        return getBlob(containerName, key, GetOptions.NONE);
    }

    /**
     * This implementation invokes {@link #deleteAndVerifyContainerGone}
     */
    @Override
    public void deleteContainer(String containerName) {
        deleteAndVerifyContainerGone(containerName);
    }

    @Override
    public Set<? extends Location> listAssignableLocations() {
        return locations.get();
    }

    /**
     * default maxResults is 1000
     */
    @Override
    public PageSet<? extends StorageMetadata> list(final String containerName, ListContainerOptions options) {
        if (options.getDir() != null && options.getPrefix() != null) {
            throw new IllegalArgumentException("Cannot set both prefix and directory");
        }

        if ((options.getDir() != null || options.isRecursive()) && (options.getDelimiter() != null)) {
            throw new IllegalArgumentException("Cannot set the delimiter if directory or recursive is set");
        }

        // Check if the container exists
        if (!storageStrategy.containerExists(containerName))
            throw cnfe(containerName);

        // Loading blobs from container
        Iterable<String> blobBelongingToContainer = null;
        try {
            blobBelongingToContainer = storageStrategy.getBlobKeysInsideContainer(containerName);
        } catch (IOException e) {
            logger.error(e, "An error occurred loading blobs contained into container %s", containerName);
            propagate(e);
        }

        blobBelongingToContainer = Iterables.filter(blobBelongingToContainer, new Predicate<String>() {
            @Override
            public boolean apply(String key) {
                // ignore folders
                return storageStrategy.blobExists(containerName, key);
            }
        });
        SortedSet<StorageMetadata> contents = newTreeSet(
                FluentIterable.from(blobBelongingToContainer).transform(new Function<String, StorageMetadata>() {
                    @Override
                    public StorageMetadata apply(String key) {
                        Blob oldBlob = loadBlob(containerName, key);
                        if (oldBlob == null) {
                            return null;
                        }
                        checkState(oldBlob.getMetadata() != null,
                                "blob " + containerName + "/" + key + " has no metadata");
                        MutableBlobMetadata md = BlobStoreUtils.copy(oldBlob.getMetadata());
                        md.setSize(oldBlob.getMetadata().getSize());
                        return md;
                    }
                }).filter(Predicates.<StorageMetadata>notNull()));

        String marker = null;
        if (options != null) {
            if (options.getDir() != null && !options.getDir().isEmpty()) {
                contents = filterDirectory(contents, options);
            } else if (options.getPrefix() != null) {
                contents = filterPrefix(contents, options);
            } else if (!options.isRecursive() || (options.getDelimiter() != null)) {
                String delimiter = options.getDelimiter() == null ? storageStrategy.getSeparator()
                        : options.getDelimiter();
                contents = extractCommonPrefixes(contents, delimiter, null);
            }

            if (options.getMarker() != null) {
                final String finalMarker = options.getMarker();
                String delimiter = storageStrategy.getSeparator();
                Optional<StorageMetadata> lastMarkerMetadata = tryFind(contents, new Predicate<StorageMetadata>() {
                    public boolean apply(StorageMetadata metadata) {
                        return metadata.getName().compareTo(finalMarker) > 0;
                    }
                });
                if (lastMarkerMetadata.isPresent()) {
                    contents = contents.tailSet(lastMarkerMetadata.get());
                } else {
                    // marker is after last key or container is empty
                    contents.clear();
                }
            }

            int maxResults = options.getMaxResults() != null ? options.getMaxResults() : 1000;
            if (!contents.isEmpty()) {
                StorageMetadata lastElement = contents.last();
                contents = newTreeSet(Iterables.limit(contents, maxResults));
                if (maxResults != 0 && !contents.contains(lastElement)) {
                    // Partial listing
                    lastElement = contents.last();
                    marker = lastElement.getName();
                }
            }

            // trim metadata, if the response isn't supposed to be detailed.
            if (!options.isDetailed()) {
                for (StorageMetadata md : contents) {
                    md.getUserMetadata().clear();
                }
            }
        }

        return new PageSetImpl<StorageMetadata>(contents, marker);
    }

    private SortedSet<StorageMetadata> filterDirectory(SortedSet<StorageMetadata> contents,
            ListContainerOptions options) {
        String prefix = options.getDir();
        final String dirPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
        contents = newTreeSet(filter(contents, new Predicate<StorageMetadata>() {
            public boolean apply(StorageMetadata o) {
                return o != null && o.getName().replace(File.separatorChar, '/').startsWith(dirPrefix)
                        && !o.getName().replace(File.separatorChar, '/').equals(dirPrefix);
            }
        }));

        if (!options.isRecursive()) {
            return extractCommonPrefixes(contents, storageStrategy.getSeparator(), dirPrefix);
        }

        return contents;
    }

    private SortedSet<StorageMetadata> filterPrefix(SortedSet<StorageMetadata> contents,
            final ListContainerOptions options) {
        contents = newTreeSet(filter(contents, new Predicate<StorageMetadata>() {
            public boolean apply(StorageMetadata o) {
                return o != null && o.getName().replace(File.separatorChar, '/').startsWith(options.getPrefix());
            }
        }));

        if (options.getDelimiter() != null) {
            return extractCommonPrefixes(contents, options.getDelimiter(), options.getPrefix());
        }

        return contents;
    }

    private SortedSet<StorageMetadata> extractCommonPrefixes(SortedSet<StorageMetadata> contents, String delimiter,
            String prefix) {
        SortedSet<String> commonPrefixes = newTreeSet(transform(contents, new CommonPrefixes(prefix, delimiter)));
        commonPrefixes.remove(CommonPrefixes.NO_PREFIX);

        contents = newTreeSet(filter(contents, new DelimiterFilter(prefix, delimiter)));

        for (String o : commonPrefixes) {
            MutableStorageMetadata md = new MutableStorageMetadataImpl();
            md.setType(StorageType.RELATIVE_PATH);

            if (prefix != null) {
                o = prefix + o;
            }
            md.setName(o + delimiter);
            contents.add(md);
        }
        return contents;
    }

    private ContainerNotFoundException cnfe(final String name) {
        return new ContainerNotFoundException(name,
                String.format("container %s not in %s", name, storageStrategy.getAllContainerNames()));
    }

    @Override
    public void removeBlob(String containerName, final String key) {
        if (!storageStrategy.containerExists(containerName)) {
            throw cnfe(containerName);
        }
        storageStrategy.removeBlob(containerName, key);
    }

    @Override
    public void removeBlobs(String container, Iterable<String> names) {
        for (String name : names) {
            removeBlob(container, name);
        }
    }

    @Override
    public BlobAccess getBlobAccess(String container, String name) {
        return storageStrategy.getBlobAccess(container, name);
    }

    @Override
    public void setBlobAccess(String container, String name, BlobAccess access) {
        storageStrategy.setBlobAccess(container, name, access);
    }

    @Override
    public boolean deleteContainerIfEmpty(String containerName) {
        boolean returnVal = true;
        if (storageStrategy.containerExists(containerName)) {
            try {
                if (Iterables.isEmpty(storageStrategy.getBlobKeysInsideContainer(containerName)))
                    storageStrategy.deleteContainer(containerName);
                else
                    returnVal = false;
            } catch (IOException e) {
                logger.error(e, "An error occurred loading blobs contained into container %s", containerName);
                throw propagate(e);
            }
        }
        return returnVal;
    }

    @Override
    public boolean containerExists(String containerName) {
        return storageStrategy.containerExists(containerName);
    }

    @Override
    public PageSet<? extends StorageMetadata> list() {
        ArrayList<String> containers = new ArrayList<String>(storageStrategy.getAllContainerNames());
        Collections.sort(containers);

        return new PageSetImpl<StorageMetadata>(
                FluentIterable.from(containers).transform(new Function<String, StorageMetadata>() {
                    @Override
                    public StorageMetadata apply(String name) {
                        return storageStrategy.getContainerMetadata(name);
                    }
                }).filter(Predicates.<StorageMetadata>notNull()), null);
    }

    @Override
    public boolean createContainerInLocation(Location location, String name) {
        return storageStrategy.createContainerInLocation(name, location, CreateContainerOptions.NONE);
    }

    @Override
    public ContainerAccess getContainerAccess(String container) {
        return storageStrategy.getContainerAccess(container);
    }

    @Override
    public void setContainerAccess(String container, ContainerAccess access) {
        storageStrategy.setContainerAccess(container, access);
    }

    private Blob loadBlob(final String container, final String key) {
        logger.debug("Opening blob in container: %s - %s", container, key);
        return storageStrategy.getBlob(container, key);
    }

    private static class DelimiterFilter implements Predicate<StorageMetadata> {
        private final String prefix;
        private final String delimiter;

        public DelimiterFilter(String prefix, String delimiter) {
            this.prefix = prefix;
            this.delimiter = delimiter;
        }

        public boolean apply(StorageMetadata metadata) {
            String name = metadata.getName();
            if (prefix == null || prefix.isEmpty()) {
                return name.indexOf(delimiter) == -1;
            }
            if (name.startsWith(prefix)) {
                String unprefixedName = name.substring(prefix.length());
                if (unprefixedName.equals("")) {
                    // a blob that matches the prefix should also be returned
                    return true;
                }
                return unprefixedName.indexOf(delimiter) == -1;
            }
            return false;
        }
    }

    private static class CommonPrefixes implements Function<StorageMetadata, String> {
        private final String prefix;
        private final String delimiter;
        public static final String NO_PREFIX = "NO_PREFIX";

        public CommonPrefixes(String prefix, String delimiter) {
            this.prefix = prefix;
            this.delimiter = delimiter;
        }

        public String apply(StorageMetadata metadata) {
            String working = metadata.getName();
            if (prefix != null) {
                if (working.startsWith(prefix)) {
                    working = working.substring(prefix.length());
                } else {
                    return NO_PREFIX;
                }
            }
            if (working.indexOf(delimiter) >= 0) {
                // include the delimiter in the result
                return working.substring(0, working.indexOf(delimiter));
            } else {
                return NO_PREFIX;
            }
        }
    }

    private static HttpResponseException returnResponseException(int code) {
        HttpResponse response = HttpResponse.builder().statusCode(code).build();
        return new HttpResponseException(
                new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://stub").build()), response);
    }

    @Override
    public String putBlob(String containerName, Blob blob) {
        return putBlob(containerName, blob, PutOptions.NONE);
    }

    @Override
    public String copyBlob(String fromContainer, String fromName, String toContainer, String toName,
            CopyOptions options) {
        Blob blob = getBlob(fromContainer, fromName);
        if (blob == null) {
            throw new KeyNotFoundException(fromContainer, fromName, "while copying");
        }

        String eTag = blob.getMetadata().getETag();
        if (eTag != null) {
            eTag = maybeQuoteETag(eTag);
            if (options.ifMatch() != null && !maybeQuoteETag(options.ifMatch()).equals(eTag)) {
                throw returnResponseException(412);
            }
            if (options.ifNoneMatch() != null && maybeQuoteETag(options.ifNoneMatch()).equals(eTag)) {
                throw returnResponseException(412);
            }
        }

        Date lastModified = blob.getMetadata().getLastModified();
        if (lastModified != null) {
            if (options.ifModifiedSince() != null && lastModified.compareTo(options.ifModifiedSince()) <= 0) {
                throw returnResponseException(412);
            }
            if (options.ifUnmodifiedSince() != null && lastModified.compareTo(options.ifUnmodifiedSince()) >= 0) {
                throw returnResponseException(412);
            }
        }

        InputStream is = null;
        try {
            is = blob.getPayload().openStream();
            ContentMetadata metadata = blob.getMetadata().getContentMetadata();
            BlobBuilder.PayloadBlobBuilder builder = blobBuilder(toName).payload(is);
            Long contentLength = metadata.getContentLength();
            if (contentLength != null) {
                builder.contentLength(contentLength);
            }

            ContentMetadata contentMetadata = options.contentMetadata();
            if (contentMetadata != null) {
                String cacheControl = contentMetadata.getCacheControl();
                if (cacheControl != null) {
                    builder.cacheControl(cacheControl);
                }
                String contentDisposition = contentMetadata.getContentDisposition();
                if (contentDisposition != null) {
                    builder.contentDisposition(contentDisposition);
                }
                String contentEncoding = contentMetadata.getContentEncoding();
                if (contentEncoding != null) {
                    builder.contentEncoding(contentEncoding);
                }
                String contentLanguage = contentMetadata.getContentLanguage();
                if (contentLanguage != null) {
                    builder.contentLanguage(contentLanguage);
                }
                String contentType = contentMetadata.getContentType();
                if (contentType != null) {
                    builder.contentType(contentType);
                }
            } else {
                builder.cacheControl(metadata.getCacheControl())
                        .contentDisposition(metadata.getContentDisposition())
                        .contentEncoding(metadata.getContentEncoding())
                        .contentLanguage(metadata.getContentLanguage()).contentType(metadata.getContentType());
            }

            Map<String, String> userMetadata = options.userMetadata();
            if (userMetadata != null) {
                builder.userMetadata(userMetadata);
            } else {
                builder.userMetadata(blob.getMetadata().getUserMetadata());
            }
            return putBlob(toContainer, builder.build());
        } catch (IOException ioe) {
            throw Throwables.propagate(ioe);
        } finally {
            Closeables2.closeQuietly(is);
        }
    }

    private void copyPayloadHeadersToBlob(Payload payload, Blob blob) {
        blob.getAllHeaders().putAll(contentMetadataCodec.toHeaders(payload.getContentMetadata()));
    }

    @Override
    public boolean blobExists(String containerName, String key) {
        if (!storageStrategy.containerExists(containerName))
            throw cnfe(containerName);
        return storageStrategy.blobExists(containerName, key);
    }

    @Override
    public Blob getBlob(String containerName, String key, GetOptions options) {
        logger.debug("Retrieving blob with key %s from container %s", key, containerName);
        // If the container doesn't exist, an exception is thrown
        if (!storageStrategy.containerExists(containerName)) {
            logger.debug("Container %s does not exist", containerName);
            throw cnfe(containerName);
        }
        // If the blob doesn't exist, a null object is returned
        if (!storageStrategy.blobExists(containerName, key)) {
            logger.debug("Item %s does not exist in container %s", key, containerName);
            return null;
        }

        Blob blob = loadBlob(containerName, key);

        if (options != null) {
            String eTag = blob.getMetadata().getETag();
            if (eTag != null) {
                eTag = maybeQuoteETag(eTag);
                if (options.getIfMatch() != null) {
                    if (!eTag.equals(maybeQuoteETag(options.getIfMatch())))
                        throw returnResponseException(412);
                }
                if (options.getIfNoneMatch() != null) {
                    if (eTag.equals(maybeQuoteETag(options.getIfNoneMatch())))
                        throw returnResponseException(304);
                }
            }
            if (options.getIfModifiedSince() != null) {
                Date modifiedSince = options.getIfModifiedSince();
                if (blob.getMetadata().getLastModified().before(modifiedSince)) {
                    HttpResponse response = HttpResponse.builder().statusCode(304).build();
                    throw new HttpResponseException(String.format("%1$s is before %2$s",
                            blob.getMetadata().getLastModified(), modifiedSince), null, response);
                }

            }
            if (options.getIfUnmodifiedSince() != null) {
                Date unmodifiedSince = options.getIfUnmodifiedSince();
                if (blob.getMetadata().getLastModified().after(unmodifiedSince)) {
                    HttpResponse response = HttpResponse.builder().statusCode(412).build();
                    throw new HttpResponseException(String.format("%1$s is after %2$s",
                            blob.getMetadata().getLastModified(), unmodifiedSince), null, response);
                }
            }
            blob = copyBlob(blob);

            if (options.getRanges() != null && !options.getRanges().isEmpty()) {
                long size = 0;
                ImmutableList.Builder<ByteSource> streams = ImmutableList.builder();

                // Try to convert payload to ByteSource, otherwise wrap it.
                ByteSource byteSource;
                try {
                    byteSource = (ByteSource) blob.getPayload().getRawContent();
                } catch (ClassCastException cce) {
                    try {
                        byteSource = ByteSource
                                .wrap(ByteStreams2.toByteArrayAndClose(blob.getPayload().openStream()));
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }

                for (String s : options.getRanges()) {
                    // HTTP uses a closed interval while Java array indexing uses a
                    // half-open interval.
                    long offset = 0;
                    long last = blob.getPayload().getContentMetadata().getContentLength() - 1;
                    if (s.startsWith("-")) {
                        offset = last - Long.parseLong(s.substring(1)) + 1;
                        if (offset < 0) {
                            offset = 0;
                        }
                    } else if (s.endsWith("-")) {
                        offset = Long.parseLong(s.substring(0, s.length() - 1));
                    } else if (s.contains("-")) {
                        String[] firstLast = s.split("\\-");
                        offset = Long.parseLong(firstLast[0]);
                        last = Long.parseLong(firstLast[1]);
                    } else {
                        throw new IllegalArgumentException("illegal range: " + s);
                    }

                    if (offset >= blob.getPayload().getContentMetadata().getContentLength()) {
                        throw new IllegalArgumentException("illegal range: " + s);
                    }
                    if (last + 1 > blob.getPayload().getContentMetadata().getContentLength()) {
                        last = blob.getPayload().getContentMetadata().getContentLength() - 1;
                    }
                    streams.add(byteSource.slice(offset, last - offset + 1));
                    size += last - offset + 1;
                    blob.getAllHeaders().put(HttpHeaders.CONTENT_RANGE, "bytes " + offset + "-" + last + "/"
                            + blob.getPayload().getContentMetadata().getContentLength());
                }
                ContentMetadata cmd = blob.getPayload().getContentMetadata();
                blob.setPayload(ByteSource.concat(streams.build()));
                HttpUtils.copy(cmd, blob.getPayload().getContentMetadata());
                blob.getPayload().getContentMetadata().setContentLength(size);
                blob.getMetadata().setSize(size);
            }
        }
        checkNotNull(blob.getPayload(), "payload " + blob);
        return blob;
    }

    @Override
    public BlobMetadata blobMetadata(String containerName, String key) {
        try {
            Blob blob = getBlob(containerName, key);
            return blob != null ? (BlobMetadata) BlobStoreUtils.copy(blob.getMetadata()) : null;
        } catch (RuntimeException e) {
            if (size(Iterables.filter(getCausalChain(e), KeyNotFoundException.class)) >= 1)
                return null;
            throw e;
        }
    }

    private Blob copyBlob(Blob blob) {
        Blob returnVal = blobFactory.create(BlobStoreUtils.copy(blob.getMetadata()));
        returnVal.setPayload(blob.getPayload());
        copyPayloadHeadersToBlob(blob.getPayload(), returnVal);
        return returnVal;
    }

    private boolean deleteAndVerifyContainerGone(final String container) {
        storageStrategy.deleteContainer(container);
        return storageStrategy.containerExists(container);
    }

    @Override
    public String putBlob(String containerName, Blob blob, PutOptions options) {
        checkNotNull(containerName, "containerName must be set");
        checkNotNull(blob, "blob must be set");
        String blobKey = blob.getMetadata().getName();

        logger.debug("Put blob with key [%s] to container [%s]", blobKey, containerName);
        if (!storageStrategy.containerExists(containerName)) {
            throw cnfe(containerName);
        }

        try {
            String eTag = storageStrategy.putBlob(containerName, blob);
            setBlobAccess(containerName, blobKey, options.getBlobAccess());
            return eTag;
        } catch (IOException e) {
            String message = e.getMessage();
            if (message != null && message.startsWith("MD5 hash code mismatch")) {
                HttpResponseException exception = returnResponseException(400);
                exception.initCause(e);
                throw exception;
            }
            logger.error(e, "An error occurred storing the new blob with name [%s] to container [%s].", blobKey,
                    containerName);
            throw propagate(e);
        }
    }

    @Override
    public boolean createContainerInLocation(Location location, String container, CreateContainerOptions options) {
        return storageStrategy.createContainerInLocation(container, location, options);
    }

    @Override
    public MultipartUpload initiateMultipartUpload(String container, BlobMetadata blobMetadata,
            PutOptions options) {
        String uploadId = UUID.randomUUID().toString();
        // create a stub blob
        Blob blob = blobBuilder(MULTIPART_PREFIX + uploadId + "-" + blobMetadata.getName() + "-stub")
                .payload(ByteSource.empty()).build();
        putBlob(container, blob);
        return MultipartUpload.create(container, blobMetadata.getName(), uploadId, blobMetadata, options);
    }

    @Override
    public void abortMultipartUpload(MultipartUpload mpu) {
        List<MultipartPart> parts = listMultipartUpload(mpu);
        for (MultipartPart part : parts) {
            removeBlob(mpu.containerName(),
                    MULTIPART_PREFIX + mpu.id() + "-" + mpu.blobName() + "-" + part.partNumber());
        }
        removeBlob(mpu.containerName(), MULTIPART_PREFIX + mpu.id() + "-" + mpu.blobName() + "-stub");
    }

    @Override
    public String completeMultipartUpload(MultipartUpload mpu, List<MultipartPart> parts) {
        ImmutableList.Builder<InputStream> streams = ImmutableList.builder();
        long contentLength = 0;
        for (MultipartPart part : parts) {
            Blob blobPart = getBlob(mpu.containerName(),
                    MULTIPART_PREFIX + mpu.id() + "-" + mpu.blobName() + "-" + part.partNumber());
            contentLength += blobPart.getMetadata().getContentMetadata().getContentLength();
            InputStream is;
            try {
                is = blobPart.getPayload().openStream();
            } catch (IOException ioe) {
                throw propagate(ioe);
            }
            streams.add(is);
        }
        PayloadBlobBuilder blobBuilder = blobBuilder(mpu.blobName())
                .userMetadata(mpu.blobMetadata().getUserMetadata())
                .payload(new SequenceInputStream(Iterators.asEnumeration(streams.build().iterator())))
                .contentLength(contentLength);
        String cacheControl = mpu.blobMetadata().getContentMetadata().getCacheControl();
        if (cacheControl != null) {
            blobBuilder.cacheControl(cacheControl);
        }
        String contentDisposition = mpu.blobMetadata().getContentMetadata().getContentDisposition();
        if (contentDisposition != null) {
            blobBuilder.contentDisposition(contentDisposition);
        }
        String contentEncoding = mpu.blobMetadata().getContentMetadata().getContentEncoding();
        if (contentEncoding != null) {
            blobBuilder.contentEncoding(contentEncoding);
        }
        String contentLanguage = mpu.blobMetadata().getContentMetadata().getContentLanguage();
        if (contentLanguage != null) {
            blobBuilder.contentLanguage(contentLanguage);
        }
        // intentionally not copying MD5
        String contentType = mpu.blobMetadata().getContentMetadata().getContentType();
        if (contentType != null) {
            blobBuilder.contentType(contentType);
        }
        Date expires = mpu.blobMetadata().getContentMetadata().getExpires();
        if (expires != null) {
            blobBuilder.expires(expires);
        }

        String eTag = putBlob(mpu.containerName(), blobBuilder.build());

        for (MultipartPart part : parts) {
            removeBlob(mpu.containerName(),
                    MULTIPART_PREFIX + mpu.id() + "-" + mpu.blobName() + "-" + part.partNumber());
        }
        removeBlob(mpu.containerName(), MULTIPART_PREFIX + mpu.id() + "-" + mpu.blobName() + "-stub");

        setBlobAccess(mpu.containerName(), mpu.blobName(), mpu.putOptions().getBlobAccess());

        return eTag;
    }

    @Override
    public MultipartPart uploadMultipartPart(MultipartUpload mpu, int partNumber, Payload payload) {
        String partName = MULTIPART_PREFIX + mpu.id() + "-" + mpu.blobName() + "-" + partNumber;
        Blob blob = blobBuilder(partName).payload(payload).build();
        String partETag = putBlob(mpu.containerName(), blob);
        long partSize = -1; // TODO: how to get this from payload?
        return MultipartPart.create(partNumber, partSize, partETag);
    }

    @Override
    public List<MultipartPart> listMultipartUpload(MultipartUpload mpu) {
        ImmutableList.Builder<MultipartPart> parts = ImmutableList.builder();
        ListContainerOptions options = new ListContainerOptions()
                .prefix(MULTIPART_PREFIX + mpu.id() + "-" + mpu.blobName() + "-").recursive();
        while (true) {
            PageSet<? extends StorageMetadata> pageSet = list(mpu.containerName(), options);
            for (StorageMetadata sm : pageSet) {
                if (sm.getName().endsWith("-stub")) {
                    continue;
                }
                int partNumber = Integer.parseInt(sm.getName()
                        .substring((MULTIPART_PREFIX + mpu.id() + "-" + mpu.blobName() + "-").length()));
                long partSize = -1; // TODO: could call getContentMetadata but did not above
                parts.add(MultipartPart.create(partNumber, partSize, sm.getETag()));
            }
            if (pageSet.isEmpty() || pageSet.getNextMarker() == null) {
                break;
            }
            options.afterMarker(pageSet.getNextMarker());
        }
        return parts.build();
    }

    @Override
    public List<MultipartUpload> listMultipartUploads(String container) {
        ImmutableList.Builder<MultipartUpload> mpus = ImmutableList.builder();
        ListContainerOptions options = new ListContainerOptions().prefix(MULTIPART_PREFIX).recursive();
        int uuidLength = UUID.randomUUID().toString().length();
        while (true) {
            PageSet<? extends StorageMetadata> pageSet = list(container, options);
            for (StorageMetadata sm : pageSet) {
                if (!sm.getName().endsWith("-stub")) {
                    continue;
                }
                String uploadId = sm.getName().substring(MULTIPART_PREFIX.length(),
                        MULTIPART_PREFIX.length() + uuidLength);
                String blobName = sm.getName().substring(MULTIPART_PREFIX.length() + uuidLength + 1);
                int index = blobName.lastIndexOf('-');
                blobName = blobName.substring(0, index);

                mpus.add(MultipartUpload.create(container, blobName, uploadId, null, null));
            }
            if (pageSet.isEmpty() || pageSet.getNextMarker() == null) {
                break;
            }
            options.afterMarker(pageSet.getNextMarker());
        }

        return mpus.build();
    }

    @Override
    public long getMinimumMultipartPartSize() {
        return 1;
    }

    @Override
    public long getMaximumMultipartPartSize() {
        return 5 * 1024 * 1024;
    }

    @Override
    public int getMaximumNumberOfParts() {
        return Integer.MAX_VALUE;
    }

    @Override
    public void downloadBlob(String container, String name, File destination) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void downloadBlob(String container, String name, File destination, ExecutorService executor) {
        throw new UnsupportedOperationException();
    }

    @Override
    public InputStream streamBlob(String container, String name) {
        throw new UnsupportedOperationException();
    }

    @Override
    public InputStream streamBlob(String container, String name, ExecutorService executor) {
        throw new UnsupportedOperationException();
    }

    private static String maybeQuoteETag(String eTag) {
        if (!eTag.startsWith("\"") && !eTag.endsWith("\"")) {
            eTag = "\"" + eTag + "\"";
        }
        return eTag;
    }

}