001/* 002 * (C) Copyright 2019 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 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.core.blob; 020 021import static org.nuxeo.ecm.core.blob.DigestConfiguration.DIGEST_ALGORITHM_PROPERTY; 022 023import java.io.File; 024import java.io.FileInputStream; 025import java.io.IOException; 026import java.io.InputStream; 027import java.nio.file.Files; 028import java.nio.file.Path; 029import java.util.Map; 030 031import org.apache.logging.log4j.LogManager; 032import org.apache.logging.log4j.Logger; 033import org.nuxeo.ecm.core.api.Blob; 034import org.nuxeo.ecm.core.blob.BlobStore.OptionalOrUnknown; 035import org.nuxeo.ecm.core.blob.binary.BinaryGarbageCollector; 036import org.nuxeo.ecm.core.blob.binary.BinaryManager; 037import org.nuxeo.runtime.api.Framework; 038 039/** 040 * A {@link BlobProvider} implemented on top of an underlying {@link BlobStore}. 041 * <p> 042 * This abstract class deals with 043 */ 044public abstract class BlobStoreBlobProvider extends AbstractBlobProvider { 045 046 /** @since 11.2 */ 047 public static final String KEY_STRATEGY_PROPERTY = "keyStrategy"; 048 049 /** @since 11.2 */ 050 public static final String MANAGED_KEY_STRATEGY = "managed"; 051 052 /** @since 11.2 */ 053 public static final String DIGEST_KEY_STRATEGY = "digest"; 054 055 public BlobStore store; 056 057 @Override 058 public void initialize(String blobProviderId, Map<String, String> properties) throws IOException { 059 super.initialize(blobProviderId, properties); 060 store = getBlobStore(blobProviderId, properties); 061 } 062 063 protected abstract BlobStore getBlobStore(String blobProviderId, Map<String, String> properties) throws IOException; 064 065 /** @since 11.2 */ 066 public KeyStrategy getKeyStrategy() { 067 boolean hasDigest = properties.get(DIGEST_ALGORITHM_PROPERTY) != null; 068 KeyStrategy keyStrategy; 069 if (isRecordMode() && !hasDigest) { 070 keyStrategy = KeyStrategyDocId.instance(); 071 } else { 072 String strKeyStrategy = properties.getOrDefault(KEY_STRATEGY_PROPERTY, DIGEST_KEY_STRATEGY); 073 keyStrategy = new KeyStrategyDigest(getDigestAlgorithm()); 074 if (MANAGED_KEY_STRATEGY.equals(strKeyStrategy)) { 075 keyStrategy = new KeyStrategyManaged(keyStrategy); 076 } 077 } 078 return keyStrategy; 079 } 080 081 /** The digest algorithm to use for the default key strategy. */ 082 protected abstract String getDigestAlgorithm(); 083 084 @Override 085 public BinaryManager getBinaryManager() { 086 return null; 087 } 088 089 @Override 090 public boolean supportsSync() { 091 return supportsUserUpdate(); 092 } 093 094 @Override 095 public BinaryGarbageCollector getBinaryGarbageCollector() { 096 return store.getBinaryGarbageCollector(); 097 } 098 099 protected String stripBlobKeyPrefix(String key) { 100 int colon = key.indexOf(':'); 101 if (colon >= 0 && key.substring(0, colon).equals(blobProviderId)) { 102 key = key.substring(colon + 1); 103 } 104 return key; 105 } 106 107 @Override 108 public String writeBlob(BlobContext blobContext) throws IOException { 109 String key = store.writeBlob(blobContext); 110 fixupDigest(blobContext.blob, key); 111 return key; 112 } 113 114 @Override 115 public String writeBlob(Blob blob) throws IOException { 116 if (isRecordMode()) { 117 throw new UnsupportedOperationException("Cannot write blob directly without context in record mode"); 118 } 119 return writeBlob(new BlobContext(blob)); 120 } 121 122 @Override 123 public InputStream getStream(ManagedBlob blob) throws IOException { 124 String blobKey = blob.getKey(); 125 return getStream(blobKey, null); 126 } 127 128 @Override 129 public InputStream getStream(String blobKey, ByteRange byteRange) throws IOException { 130 String key = stripBlobKeyPrefix(blobKey); 131 if (byteRange != null) { 132 if (!allowByteRange()) { 133 throw new UnsupportedOperationException("Cannot use byte ranges in keys"); 134 } 135 key = AbstractBlobStore.setByteRangeInKey(key, byteRange); 136 } 137 OptionalOrUnknown<InputStream> streamOpt = store.getStream(key); 138 if (streamOpt.isKnown()) { 139 if (!streamOpt.isPresent()) { 140 throw new IOException("Missing blob: " + key); 141 } 142 return streamOpt.get(); 143 } else { 144 // underlying store is low-level and doesn't have a stream available 145 // this should only happen in test situations, in real life there's a cache in front 146 boolean returned = false; 147 Path tmp = Framework.createTempFilePath("bin_", ".tmp"); 148 try { 149 boolean found = store.readBlob(key, tmp); 150 if (!found) { 151 throw new IOException("Missing blob: " + key); 152 } 153 AutoDeleteFileInputStream stream = new AutoDeleteFileInputStream(tmp); 154 returned = true; 155 return stream; 156 } finally { 157 if (!returned) { 158 Files.deleteIfExists(tmp); 159 } 160 } 161 } 162 } 163 164 @Override 165 public File getFile(ManagedBlob blob) { 166 String key = stripBlobKeyPrefix(blob.getKey()); 167 OptionalOrUnknown<Path> fileOpt = store.getFile(key); 168 return fileOpt.isPresent() ? fileOpt.get().toFile() : null; 169 } 170 171 /** 172 * A {@link FileInputStream} that deletes its underlying file when it is closed. 173 */ 174 public static class AutoDeleteFileInputStream extends FileInputStream { 175 176 private static final Logger log = LogManager.getLogger(AutoDeleteFileInputStream.class); 177 178 protected Path file; 179 180 public AutoDeleteFileInputStream(Path file) throws IOException { 181 super(file.toFile()); 182 this.file = file; 183 } 184 185 @Override 186 public void close() throws IOException { 187 try { 188 super.close(); 189 } finally { 190 if (file != null) { 191 try { 192 Files.deleteIfExists(file); 193 } catch (IOException e) { 194 log.warn(e, e); 195 } 196 // attempt delete only once, even if close() is called several times 197 file = null; 198 } 199 } 200 } 201 } 202 203 @Override 204 public Blob readBlob(BlobInfo blobInfo) throws IOException { 205 ManagedBlob blob = new SimpleManagedBlob(blobProviderId, blobInfo); // calls back to #getStream 206 fixupDigest(blob, blob.getKey()); 207 return blob; 208 } 209 210 /** 211 * Fixup of the blob's digest, if possible. 212 * 213 * @param blob the blob 214 * @param key the key 215 * @since 11.2 216 */ 217 protected void fixupDigest(Blob blob, String key) { 218 if (blob.getDigest() == null && store.getKeyStrategy().useDeDuplication()) { 219 blob.setDigest(key); 220 } 221 } 222 223 @Override 224 public void updateBlob(BlobUpdateContext blobUpdateContext) throws IOException { 225 store.writeBlobProperties(blobUpdateContext); 226 } 227 228 @Override 229 public void deleteBlob(BlobContext blobContext) { 230 store.deleteBlob(blobContext); 231 } 232 233}