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}