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:[email protected]">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 protected BinaryGarbageCollector instantiateGarbageCollector() { 123 return new AzureGarbageCollector(this); 124 } 125 126 protected FileStorage getFileStorage() { 127 return new AzureFileStorage(container, prefix); 128 } 129 130 @Override 131 protected URI getRemoteUri(String digest, ManagedBlob blob, HttpServletRequest servletRequest) throws IOException { 132 try { 133 CloudBlockBlob blockBlobReference = container.getBlockBlobReference(digest); 134 SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy(); 135 policy.setPermissionsFromString("r"); 136 137 Instant endDateTime = LocalDateTime.now() 138 .plusSeconds(directDownloadExpire) 139 .atZone(ZoneId.systemDefault()) 140 .toInstant(); 141 policy.setSharedAccessExpiryTime(Date.from(endDateTime)); 142 143 SharedAccessBlobHeaders headers = new SharedAccessBlobHeaders(); 144 headers.setContentDisposition(getContentDispositionHeader(blob, servletRequest)); 145 headers.setContentType(getContentTypeHeader(blob)); 146 147 String sas = blockBlobReference.generateSharedAccessSignature(policy, headers, null); 148 149 CloudBlockBlob signedBlob = new CloudBlockBlob(blockBlobReference.getUri(), 150 new StorageCredentialsSharedAccessSignature(sas)); 151 return signedBlob.getQualifiedUri(); 152 } catch (URISyntaxException | InvalidKeyException | StorageException e) { 153 throw new IOException(e); 154 } 155 } 156 157 protected String getContentDispositionHeader(Blob blob, HttpServletRequest servletRequest) { 158 // Azure will do the %-encoding itself, pass it a String directly 159 return "attachment; filename*=UTF-8''" + blob.getFilename(); 160 } 161 162 protected void removeBinary(String digest) { 163 try { 164 container.getBlockBlobReference(prefix + digest).delete(); 165 } catch (StorageException | URISyntaxException e) { 166 log.error("Unable to remove binary " + digest, e); 167 } 168 } 169 170 @Override 171 public void removeBinaries(Collection<String> digests) { 172 digests.forEach(this::removeBinary); 173 } 174}