/** * Copyright (c) 2011-2012 Optimax Software Ltd. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Optimax Software, ElasticInbox, nor the names * of its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.elasticinbox.core.blob.store; import static com.elasticinbox.core.blob.store.BlobStoreConstants.*; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.security.GeneralSecurityException; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import org.jclouds.blobstore.BlobStore; import org.jclouds.blobstore.BlobStoreContext; import org.jclouds.ContextBuilder; import org.jclouds.blobstore.domain.BlobBuilder; import org.jclouds.filesystem.reference.FilesystemConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.elasticinbox.common.utils.Assert; import com.elasticinbox.config.Configurator; import com.elasticinbox.config.blob.BlobStoreProfile; import com.elasticinbox.core.blob.BlobUtils; import com.elasticinbox.core.log.JcloudsSlf4JLoggingModule; import com.google.common.collect.ImmutableSet; /** * This is a proxy class for jClouds Blobstore API. * <p> * Each blob store has its own configuration profile {@link BlobStoreProfile} * which contains connection information. Connections to the blob stores (such * as S3, OpenStack, etc.) are established on demand and cached. * * @author Rustam Aliyev * @see {@link BlobStoreProfile} * @see {@link BlobStore} * @see {@link BlobStoreContext} * @see <a href="http://www.jclouds.org/">jClouds</a> */ public final class CloudStoreProxy { private static final Logger logger = LoggerFactory.getLogger(CloudStoreProxy.class); private static final String PROVIDER_FILESYSTEM = "filesystem"; private static final String PROVIDER_TRANSIENT = "transient"; private static ConcurrentHashMap<String, BlobStoreContext> blobStoreContexts = new ConcurrentHashMap<String, BlobStoreContext>(); /** * Store Blob * * @param blobName * Blob filename including relative path * @param profileName * Blob store profile name * @param in * Payload * @param size * Payload size in bytes * @return * @throws IOException */ public static void write(final String blobName, final String profileName, InputStream in, final Long size) throws IOException, GeneralSecurityException { Assert.notNull(in, "No data to store"); Assert.notNull(size, "Blob size must be specified"); final String container = Configurator.getBlobStoreProfile(profileName).getContainer(); BlobStoreContext context = getBlobStoreContext(profileName); logger.debug("Storing blob {} on {}", blobName, profileName); BlobStore blobStore = context.getBlobStore(); BlobBuilder blobBuilder = blobStore.blobBuilder(blobName).payload(in).contentLength(size); // store blob blobStore.putBlob(container, blobBuilder.build()); } /** * Read Blob contents * * @param uri * @return */ public static InputStream read(URI uri) { // check if blob was stored for the message Assert.notNull(uri, "URI cannot be null"); logger.debug("Reading blob {}", uri); String profileName = uri.getHost(); String container = Configurator.getBlobStoreProfile(profileName).getContainer(); BlobStoreContext context = getBlobStoreContext(profileName); String path = BlobUtils.relativize(uri.getPath()); InputStream in = context.getBlobStore() .getBlob(container, path) .getPayload().getInput(); return in; } /** * Delete blob * * @param uri */ public static void delete(URI uri) { // check if blob was stored for the message, skip if not if (uri == null) { return; } logger.debug("Deleting blob {}", uri); String profileName = uri.getHost(); BlobStoreProfile profile = Configurator.getBlobStoreProfile(profileName); String path = BlobUtils.relativize(uri.getPath()); if (profile.getProvider().equals(PROVIDER_FILESYSTEM)) { // Following part is added as a replacement for jClouds delete due to // performance issues with jClouds String fileName = new StringBuilder(profile.getEndpoint()) .append(File.separator).append(profile.getContainer()) .append(File.separator).append(path).toString(); File f = new File(fileName); // If file does not exist, skip if (!f.exists()) { return; } // Make sure the file is writable Assert.isTrue(f.canWrite(), "File is write protected: " + fileName); // Check if it is a directory Assert.isTrue(!f.isDirectory(), "Can't delete directory: " + fileName); // Attempt to delete it boolean success = f.delete(); Assert.isTrue(success, "Deletion failed"); } else { String container = profile.getContainer(); BlobStoreContext context = getBlobStoreContext(profileName); context.getBlobStore().removeBlob(container, path); } } /** * Build {@link BlobStoreContext} from blob profile * * @param profile * blob store profile name * @return */ private static BlobStoreContext getBlobStoreContext(String profileName) { if(blobStoreContexts.containsKey(profileName)) { return blobStoreContexts.get(profileName); } else { synchronized (CloudStoreProxy.class) { logger.debug("Creating new connection for '{}' blob store.", profileName); Properties properties = new Properties(); BlobStoreProfile profile = Configurator.getBlobStoreProfile(profileName); ContextBuilder contextBuilder = ContextBuilder.newBuilder(profile.getProvider()); if (profile.getProvider().equals(PROVIDER_FILESYSTEM)) { // use endpoint as fs basedir, see: http://code.google.com/p/jclouds/issues/detail?id=776 properties.setProperty(FilesystemConstants.PROPERTY_BASEDIR, profile.getEndpoint()); contextBuilder.endpoint(profile.getEndpoint()); //properties.setProperty(PROPERTY_CREDENTIAL, "dummy"); } else if (BLOBSTORE_PROVIDERS.contains(profile.getProvider())) { if (profile.getEndpoint() != null) { contextBuilder.endpoint(profile.getEndpoint()); } if (profile.getApiversion() != null) { contextBuilder.apiVersion(profile.getApiversion()); } if (profile.getIdentity() != null && profile.getCredential() != null) { contextBuilder.credentials(profile.getIdentity(), profile.getCredential()); } } else { throw new UnsupportedOperationException( "Unsupported Blobstore provider: " + profile.getProvider()); } // get a context with filesystem that offers the portable BlobStore api BlobStoreContext context = contextBuilder .overrides(properties) .modules(ImmutableSet.of(new JcloudsSlf4JLoggingModule())) .buildView(BlobStoreContext.class); // create container for transient store if(profile.getProvider().equals(PROVIDER_TRANSIENT)) { context.getBlobStore().createContainerInLocation(null, profile.getContainer()); } blobStoreContexts.put(profileName, context); } return blobStoreContexts.get(profileName); } } /** * Close all blob store connections */ public static synchronized void closeAll() { for (String profileName : blobStoreContexts.keySet()) { blobStoreContexts.get(profileName).close(); blobStoreContexts.remove(profileName); } } }