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    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}