├── src └── main │ ├── resources │ └── application.properties │ └── java │ └── com │ └── zyka │ └── sample │ └── blob │ ├── repository │ └── StreamingFileRepository.java │ ├── helper │ └── LobHelper.java │ ├── Main.java │ ├── model │ └── StreamingFileRecord.java │ └── FileStoreController.java ├── Readme.MD ├── .gitignore └── pom.xml /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Allows access Hibernate session factory from spring context 2 | spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext 3 | spring.http.multipart.max-file-size=1000MB 4 | spring.http.multipart.max-request-size=1000MB 5 | 6 | spring.datasource.url=jdbc:postgresql://localhost:5432/ 7 | spring.jpa.hibernate.ddl-auto=create-drop -------------------------------------------------------------------------------- /src/main/java/com/zyka/sample/blob/repository/StreamingFileRepository.java: -------------------------------------------------------------------------------- 1 | package com.zyka.sample.blob.repository; 2 | 3 | import com.zyka.sample.blob.model.StreamingFileRecord; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface StreamingFileRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /Readme.MD: -------------------------------------------------------------------------------- 1 | # Test scenario (Mac OSX) 2 | 1. Run local postgres - testing with H2 in-memory DB doesn't make sense as the blob is stored in-memory 3 | 2. Start app with decent memory limit, like `-Xmx50M` 4 | 3. Open `jvisualvm` 5 | 4. Test and watch memory consumption in `jvisualvm` 6 | ```bash 7 | mkfile -n 500m /tmp/data.dat 8 | curl -D - -F "file=@/tmp/data.dat" http://localhost:8080/blobs 9 | curl -o /tmp/data.dat.download __Location header from previous command__ 10 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Maven template 3 | target/ 4 | pom.xml.tag 5 | pom.xml.releaseBackup 6 | pom.xml.versionsBackup 7 | pom.xml.next 8 | release.properties 9 | dependency-reduced-pom.xml 10 | buildNumber.properties 11 | .mvn/timing.properties 12 | 13 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 14 | !/.mvn/wrapper/maven-wrapper.jar 15 | ### JetBrains template 16 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 17 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 18 | 19 | # User-specific stuff: 20 | .idea 21 | *.iml 22 | 23 | ## File-based project format: 24 | *.iws 25 | 26 | -------------------------------------------------------------------------------- /src/main/java/com/zyka/sample/blob/helper/LobHelper.java: -------------------------------------------------------------------------------- 1 | package com.zyka.sample.blob.helper; 2 | 3 | import java.io.InputStream; 4 | import java.sql.Blob; 5 | import org.hibernate.SessionFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | public class LobHelper { 11 | 12 | private final SessionFactory sessionFactory; 13 | 14 | @Autowired 15 | public LobHelper(SessionFactory sessionFactory) { 16 | this.sessionFactory = sessionFactory; 17 | } 18 | 19 | public Blob createBlob(InputStream content, long size) { 20 | return sessionFactory.getCurrentSession().getLobHelper().createBlob(content, size); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/zyka/sample/blob/Main.java: -------------------------------------------------------------------------------- 1 | package com.zyka.sample.blob; 2 | 3 | import org.hibernate.SessionFactory; 4 | import org.hibernate.jpa.HibernateEntityManagerFactory; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 9 | import org.springframework.transaction.annotation.EnableTransactionManagement; 10 | 11 | @EnableJpaRepositories 12 | @SpringBootApplication 13 | @EnableTransactionManagement 14 | public class Main { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(Main.class, args); 18 | } 19 | 20 | @Bean // Need to expose SessionFactory to be able to work with BLOBs 21 | public SessionFactory sessionFactory(HibernateEntityManagerFactory hemf) { 22 | return hemf.getSessionFactory(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/zyka/sample/blob/model/StreamingFileRecord.java: -------------------------------------------------------------------------------- 1 | package com.zyka.sample.blob.model; 2 | 3 | import java.sql.Blob; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.Lob; 9 | import javax.persistence.SequenceGenerator; 10 | import javax.validation.constraints.NotNull; 11 | import lombok.Data; 12 | import lombok.NoArgsConstructor; 13 | 14 | @Data 15 | @Entity 16 | @NoArgsConstructor 17 | public class StreamingFileRecord { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "FILE_RECORD_ID_SEQ") 21 | @SequenceGenerator(name="FILE_RECORD_ID_SEQ", sequenceName = "FILE_RECORD_ID_SEQ") 22 | private long id; 23 | 24 | @NotNull 25 | private String name; 26 | 27 | @Lob 28 | @NotNull 29 | private Blob data; 30 | 31 | public StreamingFileRecord(String name, Blob data) { 32 | this.name = name; 33 | this.data = data; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 1.5.6.RELEASE 11 | 12 | 13 | com.zyka.sample.blob 14 | blob-sample 15 | 1.0-SNAPSHOT 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-web 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-jpa 26 | 27 | 28 | 29 | org.postgresql 30 | postgresql 31 | 32 | 33 | 34 | org.projectlombok 35 | lombok 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/zyka/sample/blob/FileStoreController.java: -------------------------------------------------------------------------------- 1 | package com.zyka.sample.blob; 2 | 3 | import com.zyka.sample.blob.helper.LobHelper; 4 | import com.zyka.sample.blob.model.StreamingFileRecord; 5 | import com.zyka.sample.blob.repository.StreamingFileRepository; 6 | import java.io.IOException; 7 | import java.net.URI; 8 | import java.net.URISyntaxException; 9 | import java.sql.SQLException; 10 | import javax.servlet.http.HttpServletResponse; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.tomcat.util.http.fileupload.IOUtils; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.transaction.annotation.Transactional; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RequestMethod; 20 | import org.springframework.web.bind.annotation.RequestPart; 21 | import org.springframework.web.bind.annotation.RestController; 22 | import org.springframework.web.multipart.MultipartFile; 23 | 24 | @Slf4j 25 | @RestController 26 | public class FileStoreController { 27 | 28 | private final StreamingFileRepository streamingFileRepository; 29 | private final LobHelper lobCreator; 30 | 31 | @Autowired 32 | public FileStoreController(StreamingFileRepository streamingFileRepository, LobHelper lobCreator) { 33 | this.streamingFileRepository = streamingFileRepository; 34 | this.lobCreator = lobCreator; 35 | } 36 | 37 | @Transactional 38 | @RequestMapping(value = "/blobs", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) 39 | public ResponseEntity store(@RequestPart("file") MultipartFile multipartFile) throws IOException, SQLException, URISyntaxException { 40 | log.info("Persisting new file: {}", multipartFile.getOriginalFilename()); 41 | StreamingFileRecord streamingFileRecord = new StreamingFileRecord(multipartFile.getOriginalFilename(), lobCreator.createBlob(multipartFile.getInputStream(), multipartFile.getSize())); 42 | 43 | streamingFileRecord = streamingFileRepository.save(streamingFileRecord); 44 | 45 | log.info("Persisted {} with id: {}", multipartFile.getOriginalFilename(), streamingFileRecord.getId()); 46 | return ResponseEntity.created(new URI("http://localhost:8080/blobs/" + streamingFileRecord.getId())).build(); 47 | } 48 | 49 | @Transactional 50 | @RequestMapping(value = "/blobs/{id}", method = RequestMethod.GET) 51 | public void load(@PathVariable("id") long id, HttpServletResponse response) throws SQLException, IOException { 52 | log.info("Loading file id: {}", id); 53 | StreamingFileRecord record = streamingFileRepository.findOne(id); 54 | 55 | response.addHeader("Content-Disposition", "attachment; filename=" + record.getName()); 56 | IOUtils.copy(record.getData().getBinaryStream(), response.getOutputStream()); 57 | log.info("Sent file id: {}", id); 58 | } 59 | } 60 | --------------------------------------------------------------------------------