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}