001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo
016 */
017
018package org.nuxeo.ecm.blob.azure;
019
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.FileOutputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.net.URISyntaxException;
027
028import org.apache.commons.codec.binary.Hex;
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.nuxeo.ecm.core.blob.binary.FileStorage;
032
033import com.microsoft.azure.storage.StorageErrorCode;
034import com.microsoft.azure.storage.StorageException;
035import com.microsoft.azure.storage.blob.CloudBlobContainer;
036import com.microsoft.azure.storage.blob.CloudBlockBlob;
037import com.microsoft.azure.storage.core.Base64;
038
039/**
040 * @author <a href="mailto:ak@nuxeo.com">Arnaud Kervern</a>
041 * @since 7.10
042 */
043public class AzureFileStorage implements FileStorage {
044
045    private static final Log log = LogFactory.getLog(AzureFileStorage.class);
046
047    protected CloudBlobContainer container;
048
049    public AzureFileStorage(CloudBlobContainer container) {
050        this.container = container;
051    }
052
053    @Override
054    public void storeFile(String digest, File file) throws IOException {
055        long t0 = 0;
056        if (log.isDebugEnabled()) {
057            t0 = System.currentTimeMillis();
058            log.debug("storing blob " + digest + " to Azure");
059        }
060        CloudBlockBlob blob;
061        try {
062            blob = container.getBlockBlobReference(digest);
063            if (blob.exists()) {
064                if (isBlobDigestCorrect(digest, blob)) {
065                    if (log.isDebugEnabled()) {
066                        log.debug("blob " + digest + " is already in Azure");
067                    }
068                    return;
069                }
070            }
071
072            try (InputStream is = new FileInputStream(file)) {
073                blob.upload(is, file.length());
074            }
075        } catch (StorageException | URISyntaxException e) {
076            throw new IOException(e);
077        } finally {
078            if (log.isDebugEnabled()) {
079                long dtms = System.currentTimeMillis() - t0;
080                log.debug("stored blob " + digest + " to Azure in " + dtms + "ms");
081            }
082        }
083    }
084
085    @Override
086    public boolean fetchFile(String digest, File file) throws IOException {
087        long t0 = 0;
088        if (log.isDebugEnabled()) {
089            t0 = System.currentTimeMillis();
090            log.debug("fetching blob " + digest + " from Azure");
091        }
092        try {
093            CloudBlockBlob blob = container.getBlockBlobReference(digest);
094            if (!(blob.exists() && isBlobDigestCorrect(digest, blob))) {
095                log.error("Invalid ETag in Azure, AzDigest=" + blob.getProperties().getContentMD5() + " digest="
096                        + digest);
097                return false;
098            }
099            try (OutputStream os = new FileOutputStream(file)) {
100                blob.download(os);
101            }
102            return true;
103        } catch (URISyntaxException e) {
104            throw new IOException(e);
105        } catch (StorageException e) {
106            return false;
107        } finally {
108            if (log.isDebugEnabled()) {
109                long dtms = System.currentTimeMillis() - t0;
110                log.debug("fetched blob " + digest + " from Azure in " + dtms + "ms");
111            }
112        }
113    }
114
115    @Override
116    public Long fetchLength(String digest) throws IOException {
117        long t0 = 0;
118        if (log.isDebugEnabled()) {
119            t0 = System.currentTimeMillis();
120            log.debug("fetching blob length " + digest + " from Azure");
121        }
122        try {
123            CloudBlockBlob blob = container.getBlockBlobReference(digest);
124            if (!(blob.exists() && isBlobDigestCorrect(digest, blob))) {
125                log.error("Invalid ETag in Azure, AzDigest=" + blob.getProperties().getContentMD5() + " digest="
126                        + digest);
127                return null;
128            }
129            return blob.getProperties().getLength();
130        } catch (URISyntaxException e) {
131            throw new IOException(e);
132        } catch (StorageException e) {
133            return null;
134        } finally {
135            if (log.isDebugEnabled()) {
136                long dtms = System.currentTimeMillis() - t0;
137                log.debug("fetched blob length " + digest + " from Azure in " + dtms + "ms");
138            }
139        }
140    }
141
142    protected static boolean isMissingKey(StorageException e) {
143        return e.getErrorCode().equals(StorageErrorCode.RESOURCE_NOT_FOUND.toString());
144    }
145
146    protected static boolean isBlobDigestCorrect(String digest, CloudBlockBlob blob) {
147        return isBlobDigestCorrect(digest, blob.getProperties().getContentMD5());
148    }
149
150    protected static boolean isBlobDigestCorrect(String digest, String contentMD5) {
151        return digest.equals(decodeContentMD5(contentMD5));
152    }
153
154    protected static String decodeContentMD5(String contentMD5) {
155        try {
156            byte[] bytes = Base64.decode(contentMD5);
157            return Hex.encodeHexString(bytes);
158        } catch (IllegalArgumentException e) {
159            return null;
160        }
161    }
162}