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 isValidBlobId(String theNewBlobId) { 085 return !StringUtils.containsAny(theNewBlobId, '\\', '/', '|', '.'); 086 } 087 088 @Nonnull 089 @Override 090 public StoredDetails storeBlob( 091 IIdType theResourceId, 092 String theBlobIdOrNull, 093 String theContentType, 094 InputStream theInputStream, 095 RequestDetails theRequestDetails) 096 throws IOException { 097 098 String id = super.provideIdForNewBlob(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 fetchBlobDetails(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 writeBlob(IIdType theResourceId, String theBlobId, OutputStream theOutputStream) throws IOException { 149 InputStream inputStream = getInputStream(theResourceId, theBlobId); 150 151 if (inputStream != null) { 152 try (inputStream) { 153 IOUtils.copy(inputStream, theOutputStream); 154 theOutputStream.close(); 155 } 156 } 157 158 return false; 159 } 160 161 @Nullable 162 private InputStream getInputStream(IIdType theResourceId, String theBlobId) throws FileNotFoundException { 163 File storagePath = getStoragePath(theBlobId, false); 164 InputStream inputStream = null; 165 if (storagePath != null) { 166 File file = getStorageFilename(storagePath, theResourceId, theBlobId); 167 if (file.exists()) { 168 inputStream = new FileInputStream(file); 169 } 170 } 171 return inputStream; 172 } 173 174 @Override 175 public void expungeBlob(IIdType theResourceId, String theBlobId) { 176 File storagePath = getStoragePath(theBlobId, false); 177 if (storagePath != null) { 178 File storageFile = getStorageFilename(storagePath, theResourceId, theBlobId); 179 if (storageFile.exists()) { 180 delete(storageFile, theBlobId); 181 } 182 File descriptorFile = getDescriptorFilename(storagePath, theResourceId, theBlobId); 183 if (descriptorFile.exists()) { 184 delete(descriptorFile, theBlobId); 185 } 186 } 187 } 188 189 @Override 190 public byte[] fetchBlob(IIdType theResourceId, String theBlobId) throws IOException { 191 StoredDetails details = fetchBlobDetails(theResourceId, theBlobId); 192 try (InputStream inputStream = getInputStream(theResourceId, theBlobId)) { 193 194 if (inputStream != null) { 195 return IOUtils.toByteArray(inputStream, details.getBytes()); 196 } 197 } 198 199 throw new ResourceNotFoundException( 200 Msg.code(1327) + "Unknown blob ID: " + theBlobId + " for resource ID " + theResourceId); 201 } 202 203 private void delete(File theStorageFile, String theBlobId) { 204 Validate.isTrue(theStorageFile.delete(), "Failed to delete file for blob %s", theBlobId); 205 } 206 207 @Nonnull 208 private File getDescriptorFilename(File theStoragePath, IIdType theResourceId, String theId) { 209 return getStorageFilename(theStoragePath, theResourceId, theId, ".json"); 210 } 211 212 @Nonnull 213 private File getStorageFilename(File theStoragePath, IIdType theResourceId, String theId) { 214 return getStorageFilename(theStoragePath, theResourceId, theId, ".bin"); 215 } 216 217 private File getStorageFilename(File theStoragePath, IIdType theResourceId, String theId, String theExtension) { 218 Validate.notBlank(theResourceId.getResourceType()); 219 Validate.notBlank(theResourceId.getIdPart()); 220 221 String filename = theResourceId.getResourceType() + "_" + theResourceId.getIdPart() + "_" + theId; 222 return new File(theStoragePath, filename + theExtension); 223 } 224 225 private File getStoragePath(String theId, boolean theCreate) { 226 File path = myBasePath; 227 for (int i = 0; i < 10; i++) { 228 path = new File(path, theId.substring(i, i + 1)); 229 if (!path.exists()) { 230 if (theCreate) { 231 mkdir(path); 232 } else { 233 return null; 234 } 235 } 236 } 237 return path; 238 } 239 240 private void mkdir(File theBasePath) { 241 try { 242 FileUtils.forceMkdir(theBasePath); 243 } catch (IOException e) { 244 throw new ConfigurationException(Msg.code(1328) + "Unable to create path " + myBasePath + ": " + e); 245 } 246 } 247}