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}