001package ca.uhn.fhir.jpa.binstore;
002
003/*-
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2021 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.ConfigurationException;
024import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
025import com.fasterxml.jackson.annotation.JsonInclude;
026import com.fasterxml.jackson.databind.ObjectMapper;
027import com.fasterxml.jackson.databind.SerializationFeature;
028import com.google.common.base.Charsets;
029import com.google.common.hash.HashingInputStream;
030import org.apache.commons.io.FileUtils;
031import org.apache.commons.io.IOUtils;
032import org.apache.commons.io.input.CountingInputStream;
033import org.apache.commons.lang3.Validate;
034import org.hl7.fhir.instance.model.api.IIdType;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import javax.annotation.Nonnull;
039import javax.annotation.Nullable;
040import javax.annotation.PostConstruct;
041import java.io.*;
042import java.util.Date;
043
044public class FilesystemBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
045
046        private static final Logger ourLog = LoggerFactory.getLogger(FilesystemBinaryStorageSvcImpl.class);
047        private final File myBasePath;
048        private final ObjectMapper myJsonSerializer;
049
050        public FilesystemBinaryStorageSvcImpl(String theBasePath) {
051                Validate.notBlank(theBasePath);
052
053                myBasePath = new File(theBasePath);
054
055                myJsonSerializer = new ObjectMapper();
056                myJsonSerializer.setSerializationInclusion(JsonInclude.Include.NON_NULL);
057                myJsonSerializer.enable(SerializationFeature.INDENT_OUTPUT);
058        }
059
060        @PostConstruct
061        public void start() {
062                ourLog.info("Starting binary storage service with base path: {}", myBasePath);
063
064                mkdir(myBasePath);
065        }
066
067        @Override
068        public StoredDetails storeBlob(IIdType theResourceId, String theBlobIdOrNull, String theContentType, InputStream theInputStream) throws IOException {
069                String id = super.provideIdForNewBlob(theBlobIdOrNull);
070                File storagePath = getStoragePath(id, true);
071
072                // Write binary file
073                File storageFilename = getStorageFilename(storagePath, theResourceId, id);
074                ourLog.info("Writing to file: {}", storageFilename.getAbsolutePath());
075                CountingInputStream countingInputStream = createCountingInputStream(theInputStream);
076                HashingInputStream hashingInputStream = createHashingInputStream(countingInputStream);
077                try (FileOutputStream outputStream = new FileOutputStream(storageFilename)) {
078                        IOUtils.copy(hashingInputStream, outputStream);
079                }
080
081                // Write descriptor file
082                long count = countingInputStream.getCount();
083                StoredDetails details = new StoredDetails(id, count, theContentType, hashingInputStream, new Date());
084                File descriptorFilename = getDescriptorFilename(storagePath, theResourceId, id);
085                ourLog.info("Writing to file: {}", descriptorFilename.getAbsolutePath());
086                try (FileWriter writer = new FileWriter(descriptorFilename)) {
087                        myJsonSerializer.writeValue(writer, details);
088                }
089
090                ourLog.info("Stored binary blob with {} bytes and ContentType {} for resource {}", count, theContentType, theResourceId);
091
092                return details;
093        }
094
095        @Override
096        public StoredDetails fetchBlobDetails(IIdType theResourceId, String theBlobId) throws IOException {
097                StoredDetails retVal = null;
098
099                File storagePath = getStoragePath(theBlobId, false);
100                if (storagePath != null) {
101                        File file = getDescriptorFilename(storagePath, theResourceId, theBlobId);
102                        if (file.exists()) {
103                                try (InputStream inputStream = new FileInputStream(file)) {
104                                        try (Reader reader = new InputStreamReader(inputStream, Charsets.UTF_8)) {
105                                                retVal = myJsonSerializer.readValue(reader, StoredDetails.class);
106                                        }
107                                }
108                        }
109                }
110
111                return retVal;
112        }
113
114        @Override
115        public boolean writeBlob(IIdType theResourceId, String theBlobId, OutputStream theOutputStream) throws IOException {
116                InputStream inputStream = getInputStream(theResourceId, theBlobId);
117
118                if (inputStream != null) {
119                        try {
120                                IOUtils.copy(inputStream, theOutputStream);
121                                theOutputStream.close();
122                        } finally {
123                                inputStream.close();
124                        }
125                }
126
127                return false;
128        }
129
130        @Nullable
131        private InputStream getInputStream(IIdType theResourceId, String theBlobId) throws FileNotFoundException {
132                File storagePath = getStoragePath(theBlobId, false);
133                InputStream inputStream = null;
134                if (storagePath != null) {
135                        File file = getStorageFilename(storagePath, theResourceId, theBlobId);
136                        if (file.exists()) {
137                                inputStream = new FileInputStream(file);
138                        }
139                }
140                return inputStream;
141        }
142
143        @Override
144        public void expungeBlob(IIdType theResourceId, String theBlobId) {
145                File storagePath = getStoragePath(theBlobId, false);
146                if (storagePath != null) {
147                        File storageFile = getStorageFilename(storagePath, theResourceId, theBlobId);
148                        if (storageFile.exists()) {
149                                delete(storageFile, theBlobId);
150                        }
151                        File descriptorFile = getDescriptorFilename(storagePath, theResourceId, theBlobId);
152                        if (descriptorFile.exists()) {
153                                delete(descriptorFile, theBlobId);
154                        }
155                }
156        }
157
158        @Override
159        public byte[] fetchBlob(IIdType theResourceId, String theBlobId) throws IOException {
160                StoredDetails details = fetchBlobDetails(theResourceId, theBlobId);
161                try (InputStream inputStream = getInputStream(theResourceId, theBlobId)) {
162
163                        if (inputStream != null) {
164                                return IOUtils.toByteArray(inputStream, details.getBytes());
165                        }
166
167                }
168
169                throw new ResourceNotFoundException("Unknown blob ID: " + theBlobId + " for resource ID " + theResourceId);
170        }
171
172        private void delete(File theStorageFile, String theBlobId) {
173                Validate.isTrue(theStorageFile.delete(), "Failed to delete file for blob %s", theBlobId);
174        }
175
176        @Nonnull
177        private File getDescriptorFilename(File theStoragePath, IIdType theResourceId, String theId) {
178                return getStorageFilename(theStoragePath, theResourceId, theId, ".json");
179        }
180
181        @Nonnull
182        private File getStorageFilename(File theStoragePath, IIdType theResourceId, String theId) {
183                return getStorageFilename(theStoragePath, theResourceId, theId, ".bin");
184        }
185
186        private File getStorageFilename(File theStoragePath, IIdType theResourceId, String theId, String theExtension) {
187                Validate.notBlank(theResourceId.getResourceType());
188                Validate.notBlank(theResourceId.getIdPart());
189
190                String filename = theResourceId.getResourceType() + "_" + theResourceId.getIdPart() + "_" + theId;
191                return new File(theStoragePath, filename + theExtension);
192        }
193
194        private File getStoragePath(String theId, boolean theCreate) {
195                File path = myBasePath;
196                for (int i = 0; i < 10; i++) {
197                        path = new File(path, theId.substring(i, i + 1));
198                        if (!path.exists()) {
199                                if (theCreate) {
200                                        mkdir(path);
201                                } else {
202                                        return null;
203                                }
204                        }
205                }
206                return path;
207        }
208
209        private void mkdir(File theBasePath) {
210                try {
211                        FileUtils.forceMkdir(theBasePath);
212                } catch (IOException e) {
213                        throw new ConfigurationException("Unable to create path " + myBasePath + ": " + e.toString());
214                }
215        }
216}