001/* 002 * (C) Copyright 2015-2018 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Nuxeo 018 */ 019 020package org.nuxeo.ecm.blob.azure; 021 022import java.io.IOException; 023import java.net.URI; 024import java.net.URISyntaxException; 025import java.security.InvalidKeyException; 026import java.time.Instant; 027import java.time.LocalDateTime; 028import java.time.ZoneId; 029import java.util.Collection; 030import java.util.Date; 031 032import javax.servlet.http.HttpServletRequest; 033 034import org.apache.commons.lang3.StringUtils; 035import org.apache.commons.logging.Log; 036import org.apache.commons.logging.LogFactory; 037import org.nuxeo.ecm.blob.AbstractCloudBinaryManager; 038import org.nuxeo.ecm.core.api.Blob; 039import org.nuxeo.ecm.core.blob.ManagedBlob; 040import org.nuxeo.ecm.core.blob.binary.BinaryGarbageCollector; 041import org.nuxeo.ecm.core.blob.binary.FileStorage; 042 043import com.microsoft.azure.storage.CloudStorageAccount; 044import com.microsoft.azure.storage.StorageCredentialsSharedAccessSignature; 045import com.microsoft.azure.storage.StorageException; 046import com.microsoft.azure.storage.blob.CloudBlobClient; 047import com.microsoft.azure.storage.blob.CloudBlobContainer; 048import com.microsoft.azure.storage.blob.CloudBlockBlob; 049import com.microsoft.azure.storage.blob.SharedAccessBlobHeaders; 050import com.microsoft.azure.storage.blob.SharedAccessBlobPolicy; 051 052/** 053 * @author <a href="mailto:ak@nuxeo.com">Arnaud Kervern</a> 054 * @since 7.10 055 */ 056public class AzureBinaryManager extends AbstractCloudBinaryManager { 057 058 private static final Log log = LogFactory.getLog(AzureBinaryManager.class); 059 060 private final static String STORAGE_CONNECTION_STRING = "DefaultEndpointsProtocol=%s;" + "AccountName=%s;" 061 + "AccountKey=%s"; 062 063 public static final String ENDPOINT_PROTOCOL_PROPERTY = "endpointProtocol"; 064 065 public final static String SYSTEM_PROPERTY_PREFIX = "nuxeo.storage.azure"; 066 067 public static final String ACCOUNT_NAME_PROPERTY = "account.name"; 068 069 public static final String ACCOUNT_KEY_PROPERTY = "account.key"; 070 071 public static final String CONTAINER_PROPERTY = "container"; 072 073 /** @since 10.10 */ 074 public static final String PREFIX_PROPERTY = "prefix"; 075 076 protected CloudStorageAccount storageAccount; 077 078 protected CloudBlobClient blobClient; 079 080 protected CloudBlobContainer container; 081 082 protected String prefix; 083 084 @Override 085 protected String getSystemPropertyPrefix() { 086 return SYSTEM_PROPERTY_PREFIX; 087 } 088 089 @Override 090 protected void setupCloudClient() throws IOException { 091 if (StringUtils.isBlank(properties.get(AzureBinaryManager.ACCOUNT_KEY_PROPERTY))) { 092 properties.put(AzureBinaryManager.ACCOUNT_NAME_PROPERTY, System.getenv("AZURE_STORAGE_ACCOUNT")); 093 properties.put(AzureBinaryManager.ACCOUNT_KEY_PROPERTY, System.getenv("AZURE_STORAGE_ACCESS_KEY")); 094 } 095 096 String connectionString = String.format(STORAGE_CONNECTION_STRING, 097 getProperty(ENDPOINT_PROTOCOL_PROPERTY, "https"), getProperty(ACCOUNT_NAME_PROPERTY), 098 getProperty(ACCOUNT_KEY_PROPERTY)); 099 try { 100 storageAccount = CloudStorageAccount.parse(connectionString); 101 102 blobClient = storageAccount.createCloudBlobClient(); 103 container = blobClient.getContainerReference(getProperty(CONTAINER_PROPERTY)); 104 container.createIfNotExists(); 105 } catch (URISyntaxException | InvalidKeyException | StorageException e) { 106 throw new IOException("Unable to initialize Azure binary manager", e); 107 } 108 prefix = StringUtils.defaultIfBlank(properties.get(PREFIX_PROPERTY), ""); 109 String delimiter = blobClient.getDirectoryDelimiter(); 110 if (StringUtils.isNotBlank(prefix) && !prefix.endsWith(delimiter)) { 111 prefix += delimiter; 112 } 113 if (StringUtils.isNotBlank(namespace)) { 114 // use namespace as an additional prefix 115 prefix += namespace; 116 if (!prefix.endsWith(delimiter)) { 117 prefix += delimiter; 118 } 119 } 120 } 121 122 @Override 123 protected BinaryGarbageCollector instantiateGarbageCollector() { 124 return new AzureGarbageCollector(this); 125 } 126 127 @Override 128 protected FileStorage getFileStorage() { 129 return new AzureFileStorage(container, prefix); 130 } 131 132 @Override 133 protected URI getRemoteUri(String digest, ManagedBlob blob, HttpServletRequest servletRequest) throws IOException { 134 try { 135 CloudBlockBlob blockBlobReference = container.getBlockBlobReference(digest); 136 SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy(); 137 policy.setPermissionsFromString("r"); 138 139 Instant endDateTime = LocalDateTime.now() 140 .plusSeconds(directDownloadExpire) 141 .atZone(ZoneId.systemDefault()) 142 .toInstant(); 143 policy.setSharedAccessExpiryTime(Date.from(endDateTime)); 144 145 SharedAccessBlobHeaders headers = new SharedAccessBlobHeaders(); 146 headers.setContentDisposition(getContentDispositionHeader(blob, servletRequest)); 147 headers.setContentType(getContentTypeHeader(blob)); 148 149 String sas = blockBlobReference.generateSharedAccessSignature(policy, headers, null); 150 151 CloudBlockBlob signedBlob = new CloudBlockBlob(blockBlobReference.getUri(), 152 new StorageCredentialsSharedAccessSignature(sas)); 153 return signedBlob.getQualifiedUri(); 154 } catch (URISyntaxException | InvalidKeyException | StorageException e) { 155 throw new IOException(e); 156 } 157 } 158 159 @Override 160 protected String getContentDispositionHeader(Blob blob, HttpServletRequest servletRequest) { 161 // Azure will do the %-encoding itself, pass it a String directly 162 return "attachment; filename*=UTF-8''" + blob.getFilename(); 163 } 164 165 protected void removeBinary(String digest) { 166 try { 167 container.getBlockBlobReference(prefix + digest).delete(); 168 } catch (StorageException | URISyntaxException e) { 169 log.error("Unable to remove binary " + digest, e); 170 } 171 } 172 173 @Override 174 public void removeBinaries(Collection<String> digests) { 175 digests.forEach(this::removeBinary); 176 } 177}