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