diff --git a/README.adoc b/README.adoc index 5ef7e44..3c743f6 100644 --- a/README.adoc +++ b/README.adoc @@ -1,12 +1,6 @@ ---- -tags: [files, upload, hateoas, mvc] -projects: [spring-framework, spring-hateoas] ---- :spring_version: current :spring_boot_version: 1.4.0.RELEASE -:Component: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/stereotype/Component.html :Controller: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/stereotype/Controller.html -:DispatcherServlet: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/web/servlet/DispatcherServlet.html :SpringApplication: http://docs.spring.io/spring-boot/docs/{spring_boot_version}/api/org/springframework/boot/SpringApplication.html :ResponseBody: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/web/bind/annotation/ResponseBody.html :SpringBootApplication: http://docs.spring.io/spring-boot/docs/{spring_boot_version}/api/org/springframework/boot/autoconfigure/SpringBootApplication.html @@ -15,11 +9,11 @@ projects: [spring-framework, spring-hateoas] :source-highlighter: prettify :project_id: gs-uploading-files -This guide walks you through the process of creating a server application that can receive multi-part file uploads. +This guide walks you through the process of creating a server application that can receive HTTP multi-part file uploads. == What you'll build -You will create a Spring MVC application that accepts file uploads. You will also build a simple client to upload a test file. +You will create a Spring Boot web application that accepts file uploads. You will also build a simple HTML interface to upload a test file. == What you'll need @@ -40,9 +34,11 @@ include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/ [[initial]] -== Create a configuration class +== Create an Application class -To upload files with Servlet 3.0 containers, you need to register a `MultipartConfigElement` class (which would be `` in web.xml). Thanks to Spring Boot, that bean is already registered and available! All you need to get started with this application is the following, empty configuration setup. +To start a Spring Boot MVC application, we first need a starter; here, `spring-boot-starter-thymeleaf` and `spring-boot-starter-web` are already added as dependencies. To upload files with Servlet containers, you need to register a `MultipartConfigElement` class (which would be `` in web.xml). Thanks to Spring Boot, everything is auto-configured for you! + +All you need to get started with this application is the following `Application` class. `src/main/java/hello/Application.java` [source,java] @@ -50,16 +46,12 @@ To upload files with Servlet 3.0 containers, you need to register a `MultipartCo include::initial/src/main/java/hello/Application.java[] ---- -You will soon add a Spring MVC controller, which is why you need `@SpringBootApplication`. Spring Boot automatically finds `@Controller`-marked classes and registers them with the application context. - As part of auto-configuring Spring MVC, Spring Boot will create a `MultipartConfigElement` bean and make itself ready for file uploads. -NOTE: http://tomcat.apache.org/tomcat-7.0-doc/servletapi/javax/servlet/MultipartConfigElement.html[MultipartConfigElement] is a Servlet 3.0 standard element that defines the limits on uploading files. This component is supported by all compliant containers like Tomcat and Jetty. Later on in this guide, we'll see how to configure its limits. - == Create a file upload controller -In Spring MVC, a controller is used to handle file upload requests. The following code provides the web app with the ability to upload files. +The initial application already contains a few classes to deal with storing and loading the uploaded files on disk; they're all located in the `hello.storage` package. We'll use those in our new `FileUploadController`. `src/main/java/hello/FileUploadController.java` [source,java] @@ -67,17 +59,17 @@ In Spring MVC, a controller is used to handle file upload requests. The followin include::complete/src/main/java/hello/FileUploadController.java[] ---- -The entire class is marked up with `@Controller` so Spring MVC can pick it up and look for routes. +This class is annotated with `@Controller` so Spring MVC can pick it up and look for routes. Each method is tagged with `@GetMapping` or `@PostMapping` to tie the path and the HTTP action to a particular Controller action. -Each method is tagged with `@RequestMapping` to flag the path and the HTTP action. In this case, `GET` looks up the current list of uploaded files (stored in `Application.ROOT` folder) and loads it into a Thymeleaf template. It provides a link to not only see, but "surf" to the file (letting the browser decide how to render). +In this case: -NOTE: This example uses Java 8's stream support combined with Java NIO operations to build paths, walk the directory of files, and generate Spring HATEOAS links. - -The `handleFileUpload` method is geared to handle a multi-part message: `file`. After verifying the file isn't empty, it uses Java NIO to copy the input stream to a local file. +* `GET /` looks up the current list of uploaded files from the `StorageService` and loads it into a Thymeleaf template. It calculates a link to the actual resource using `MvcUriComponentsBuilder` +* `GET /files/{filename}` loads the resource if it exists, and sends it to the browser to download using a `"Content-Disposition"` response header +* `POST /` is geared to handle a multi-part message `file` and give it to the `StorageService` for saving NOTE: In a production scenario, you more likely would store the files in a temporary location, a database, or perhaps a NoSQL store like http://docs.mongodb.org/manual/core/gridfs[Mongo's GridFS]. It's is best to NOT load up the file system of your application with content. -== Creating a barebones template +== Creating a simple HTML template To build something of interest, the following Thymeleaf template is a nice example of uploading files as well as showing what's been uploaded. @@ -98,7 +90,7 @@ This template has three parts: When configuring file uploads, it is often useful to set limits on the size of files. Imagine trying to handle a 5GB file upload! With Spring Boot, we can tune its auto-configured `MultipartConfigElement` with some property settings. -Create `src/main/resources/application.properties` and make it look like this: +Add the following properties to your existing `src/main/resources/application.properties`: `src/main/resources/application.properties` [source,java] @@ -141,6 +133,19 @@ You should then see something like this in your browser window: You successfully uploaded ! .... +== Testing your application + +There are multiple ways to test this particular feature in our application. Here's one example that leverages `MockMvc`, so it does not require to start the Servlet container: + +`src/test/java/hello/FileUploadTests.java` +[source,java] +---- +include::complete/src/test/java/hello/FileUploadTests.java[] +---- + +In those tests, we're using various mocks to set up the interactions with our Controller and the `StorageService` but also with the Servlet container itself by using `MockMultipartFile`. + +For an example of an integration test, please check out the `FileUploadIntegrationTests` class. == Summary diff --git a/complete/build.gradle b/complete/build.gradle index a52503b..d47adf8 100644 --- a/complete/build.gradle +++ b/complete/build.gradle @@ -9,14 +9,8 @@ buildscript { apply plugin: 'java' apply plugin: 'eclipse' -apply plugin: 'idea' apply plugin: 'spring-boot' -apply plugin: 'application' -run { - args 'sample.txt' -} - jar { baseName = 'gs-uploading-files' version = '0.1.0' @@ -31,8 +25,8 @@ targetCompatibility = 1.8 dependencies { compile("org.springframework.boot:spring-boot-starter-thymeleaf") - compile("org.springframework.boot:spring-boot-starter-hateoas") compile("org.springframework.boot:spring-boot-devtools") - testCompile("junit:junit") + compile("org.springframework.boot:spring-boot-configuration-processor") + testCompile("org.springframework.boot:spring-boot-starter-test") } diff --git a/complete/pom.xml b/complete/pom.xml index d77edf6..2418c0c 100644 --- a/complete/pom.xml +++ b/complete/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework @@ -21,13 +21,20 @@ org.springframework.boot - spring-boot-starter-hateoas + spring-boot-devtools + true org.springframework.boot - spring-boot-devtools + spring-boot-configuration-processor true + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/complete/src/main/java/hello/Application.java b/complete/src/main/java/hello/Application.java index e77670a..fbfd52f 100644 --- a/complete/src/main/java/hello/Application.java +++ b/complete/src/main/java/hello/Application.java @@ -1,16 +1,15 @@ package hello; -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; - +import hello.storage.StorageProperties; +import hello.storage.StorageService; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.util.FileSystemUtils; @SpringBootApplication +@EnableConfigurationProperties(StorageProperties.class) public class Application { public static void main(String[] args) { @@ -18,11 +17,10 @@ public static void main(String[] args) { } @Bean - CommandLineRunner init() { + CommandLineRunner init(StorageService storageService) { return (args) -> { - FileSystemUtils.deleteRecursively(new File(FileUploadController.ROOT)); - - Files.createDirectory(Paths.get(FileUploadController.ROOT)); + storageService.deleteAll(); + storageService.init(); }; } } diff --git a/complete/src/main/java/hello/FileUploadController.java b/complete/src/main/java/hello/FileUploadController.java index a078ab0..dd31dbf 100644 --- a/complete/src/main/java/hello/FileUploadController.java +++ b/complete/src/main/java/hello/FileUploadController.java @@ -1,83 +1,70 @@ package hello; -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import hello.storage.StorageFileNotFoundException; +import hello.storage.StorageService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import java.io.IOException; +import java.util.stream.Collectors; + @Controller public class FileUploadController { - private static final Logger log = LoggerFactory.getLogger(FileUploadController.class); - - public static final String ROOT = "upload-dir"; - - private final ResourceLoader resourceLoader; - - @Autowired - public FileUploadController(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - - @RequestMapping(method = RequestMethod.GET, value = "/") - public String provideUploadInfo(Model model) throws IOException { - - model.addAttribute("files", Files.walk(Paths.get(ROOT)) - .filter(path -> !path.equals(Paths.get(ROOT))) - .map(path -> Paths.get(ROOT).relativize(path)) - .map(path -> linkTo(methodOn(FileUploadController.class).getFile(path.toString())).withRel(path.toString())) - .collect(Collectors.toList())); - - return "uploadForm"; - } - - @RequestMapping(method = RequestMethod.GET, value = "/{filename:.+}") - @ResponseBody - public ResponseEntity getFile(@PathVariable String filename) { - - try { - return ResponseEntity.ok(resourceLoader.getResource("file:" + Paths.get(ROOT, filename).toString())); - } catch (Exception e) { - return ResponseEntity.notFound().build(); - } - } - - @RequestMapping(method = RequestMethod.POST, value = "/") - public String handleFileUpload(@RequestParam("file") MultipartFile file, - RedirectAttributes redirectAttributes) { - - if (!file.isEmpty()) { - try { - Files.copy(file.getInputStream(), Paths.get(ROOT, file.getOriginalFilename())); - redirectAttributes.addFlashAttribute("message", - "You successfully uploaded " + file.getOriginalFilename() + "!"); - } catch (IOException|RuntimeException e) { - redirectAttributes.addFlashAttribute("message", "Failued to upload " + file.getOriginalFilename() + " => " + e.getMessage()); - } - } else { - redirectAttributes.addFlashAttribute("message", "Failed to upload " + file.getOriginalFilename() + " because it was empty"); - } - - return "redirect:/"; - } + private final StorageService storageService; + + @Autowired + public FileUploadController(StorageService storageService) { + this.storageService = storageService; + } + + @GetMapping("/") + public String listUploadedFiles(Model model) throws IOException { + + model.addAttribute("files", storageService + .loadAll() + .map(path -> + MvcUriComponentsBuilder + .fromMethodName(FileUploadController.class, "serveFile", path.getFileName().toString()) + .build().toString()) + .collect(Collectors.toList())); + + return "uploadForm"; + } + + @GetMapping("/files/{filename:.+}") + @ResponseBody + public ResponseEntity serveFile(@PathVariable String filename) { + + Resource file = storageService.loadAsResource(filename); + return ResponseEntity + .ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+file.getFilename()+"\"") + .body(file); + } + + @PostMapping("/") + public String handleFileUpload(@RequestParam("file") MultipartFile file, + RedirectAttributes redirectAttributes) { + + storageService.store(file); + redirectAttributes.addFlashAttribute("message", + "You successfully uploaded " + file.getOriginalFilename() + "!"); + + return "redirect:/"; + } + + @ExceptionHandler(StorageFileNotFoundException.class) + public ResponseEntity handleStorageFileNotFound(StorageFileNotFoundException exc) { + return ResponseEntity.notFound().build(); + } } diff --git a/complete/src/main/java/hello/storage/FileSystemStorageService.java b/complete/src/main/java/hello/storage/FileSystemStorageService.java new file mode 100644 index 0000000..775ab71 --- /dev/null +++ b/complete/src/main/java/hello/storage/FileSystemStorageService.java @@ -0,0 +1,86 @@ +package hello.storage; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Service; +import org.springframework.util.FileSystemUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; + +@Service +public class FileSystemStorageService implements StorageService { + + private final Path rootLocation; + + @Autowired + public FileSystemStorageService(StorageProperties properties) { + this.rootLocation = Paths.get(properties.getLocation()); + } + + @Override + public void store(MultipartFile file) { + try { + if (file.isEmpty()) { + throw new StorageException("Failed to store empty file " + file.getOriginalFilename()); + } + Files.copy(file.getInputStream(), this.rootLocation.resolve(file.getOriginalFilename())); + } catch (IOException e) { + throw new StorageException("Failed to store file " + file.getOriginalFilename(), e); + } + } + + @Override + public Stream loadAll() { + try { + return Files.walk(this.rootLocation, 1) + .filter(path -> !path.equals(this.rootLocation)) + .map(path -> this.rootLocation.relativize(path)); + } catch (IOException e) { + throw new StorageException("Failed to read stored files", e); + } + + } + + @Override + public Path load(String filename) { + return rootLocation.resolve(filename); + } + + @Override + public Resource loadAsResource(String filename) { + try { + Path file = load(filename); + Resource resource = new UrlResource(file.toUri()); + if(resource.exists() || resource.isReadable()) { + return resource; + } + else { + throw new StorageFileNotFoundException("Could not read file: " + filename); + + } + } catch (MalformedURLException e) { + throw new StorageFileNotFoundException("Could not read file: " + filename, e); + } + } + + @Override + public void deleteAll() { + FileSystemUtils.deleteRecursively(rootLocation.toFile()); + } + + @Override + public void init() { + try { + Files.createDirectory(rootLocation); + } catch (IOException e) { + throw new StorageException("Could not initialize storage", e); + } + } +} diff --git a/complete/src/main/java/hello/storage/StorageException.java b/complete/src/main/java/hello/storage/StorageException.java new file mode 100644 index 0000000..93fbc36 --- /dev/null +++ b/complete/src/main/java/hello/storage/StorageException.java @@ -0,0 +1,12 @@ +package hello.storage; + +public class StorageException extends RuntimeException { + + public StorageException(String message) { + super(message); + } + + public StorageException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/complete/src/main/java/hello/storage/StorageFileNotFoundException.java b/complete/src/main/java/hello/storage/StorageFileNotFoundException.java new file mode 100644 index 0000000..8cd3799 --- /dev/null +++ b/complete/src/main/java/hello/storage/StorageFileNotFoundException.java @@ -0,0 +1,12 @@ +package hello.storage; + +public class StorageFileNotFoundException extends StorageException { + + public StorageFileNotFoundException(String message) { + super(message); + } + + public StorageFileNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/complete/src/main/java/hello/storage/StorageProperties.java b/complete/src/main/java/hello/storage/StorageProperties.java new file mode 100644 index 0000000..92dfeaa --- /dev/null +++ b/complete/src/main/java/hello/storage/StorageProperties.java @@ -0,0 +1,21 @@ +package hello.storage; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("storage") +public class StorageProperties { + + /** + * Folder location for storing files + */ + private String location = "upload-dir"; + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + +} diff --git a/complete/src/main/java/hello/storage/StorageService.java b/complete/src/main/java/hello/storage/StorageService.java new file mode 100644 index 0000000..44aa4ca --- /dev/null +++ b/complete/src/main/java/hello/storage/StorageService.java @@ -0,0 +1,23 @@ +package hello.storage; + +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; + +import java.nio.file.Path; +import java.util.stream.Stream; + +public interface StorageService { + + void init(); + + void store(MultipartFile file); + + Stream loadAll(); + + Path load(String filename); + + Resource loadAsResource(String filename); + + void deleteAll(); + +} diff --git a/complete/src/main/resources/application.properties b/complete/src/main/resources/application.properties index c617a42..4e26cfd 100644 --- a/complete/src/main/resources/application.properties +++ b/complete/src/main/resources/application.properties @@ -1,2 +1,2 @@ -spring.http.multipart.max-file-size: 128KB -spring.http.multipart.max-request-size: 128KB +spring.http.multipart.max-file-size=128KB +spring.http.multipart.max-request-size=128KB diff --git a/complete/src/main/resources/templates/uploadForm.html b/complete/src/main/resources/templates/uploadForm.html index c3ced53..a8c935a 100644 --- a/complete/src/main/resources/templates/uploadForm.html +++ b/complete/src/main/resources/templates/uploadForm.html @@ -17,7 +17,7 @@

diff --git a/complete/src/test/java/hello/FileUploadIntegrationTests.java b/complete/src/test/java/hello/FileUploadIntegrationTests.java new file mode 100644 index 0000000..cdd57e7 --- /dev/null +++ b/complete/src/test/java/hello/FileUploadIntegrationTests.java @@ -0,0 +1,65 @@ +package hello; + +import hello.storage.StorageService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.multipart.MultipartFile; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Matchers.any; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class FileUploadIntegrationTests { + + @Autowired + private TestRestTemplate restTemplate; + + @MockBean + private StorageService storageService; + + @LocalServerPort + private int port; + + @Test + public void shouldUploadFile() throws Exception { + ClassPathResource resource = new ClassPathResource("testupload.txt", getClass()); + + MultiValueMap map = new LinkedMultiValueMap(); + map.add("file", resource); + ResponseEntity response = this.restTemplate.postForEntity("/", map, String.class); + + assertThat(response.getStatusCode()).isEqualByComparingTo(HttpStatus.FOUND); + assertThat(response.getHeaders().getLocation().toString()).startsWith("http://localhost:" + this.port + "/"); + then(storageService).should().store(any(MultipartFile.class)); + } + + @Test + public void shouldDownloadFile() throws Exception { + ClassPathResource resource = new ClassPathResource("testupload.txt", getClass()); + given(this.storageService.loadAsResource("testupload.txt")).willReturn(resource); + + ResponseEntity response = this.restTemplate + .getForEntity("/files/{filename}", String.class, "testupload.txt"); + + assertThat(response.getStatusCodeValue()).isEqualTo(200); + assertThat(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION)) + .isEqualTo("attachment; filename=\"testupload.txt\""); + assertThat(response.getBody()).isEqualTo("Spring Framework"); + } + +} diff --git a/complete/src/test/java/hello/FileUploadTests.java b/complete/src/test/java/hello/FileUploadTests.java new file mode 100644 index 0000000..5bae35c --- /dev/null +++ b/complete/src/test/java/hello/FileUploadTests.java @@ -0,0 +1,67 @@ +package hello; + +import hello.storage.StorageFileNotFoundException; +import hello.storage.StorageService; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.nio.file.Paths; +import java.util.stream.Stream; + +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@AutoConfigureMockMvc +@SpringBootTest +public class FileUploadTests { + + @Autowired + private MockMvc mvc; + + @MockBean + private StorageService storageService; + + @Test + public void shouldListAllFiles() throws Exception { + given(this.storageService.loadAll()) + .willReturn(Stream.of(Paths.get("first.txt"), Paths.get("second.txt"))); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(model().attribute("files", + Matchers.contains("http://localhost/files/first.txt", "http://localhost/files/second.txt"))); + } + + @Test + public void shouldSaveUploadedFile() throws Exception { + MockMultipartFile multipartFile = + new MockMultipartFile("file", "test.txt", "text/plain", "Spring Framework".getBytes()); + this.mvc.perform(fileUpload("/").file(multipartFile)) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "/")); + + then(this.storageService).should().store(multipartFile); + } + + @Test + public void should404WhenMissingFile() throws Exception { + given(this.storageService.loadAsResource("test.txt")) + .willThrow(StorageFileNotFoundException.class); + + this.mvc.perform(get("/files/test.txt")) + .andExpect(status().isNotFound()); + } + +} diff --git a/complete/src/test/resources/hello/testupload.txt b/complete/src/test/resources/hello/testupload.txt new file mode 100644 index 0000000..5b97ca7 --- /dev/null +++ b/complete/src/test/resources/hello/testupload.txt @@ -0,0 +1 @@ +Spring Framework \ No newline at end of file diff --git a/initial/build.gradle b/initial/build.gradle index a52503b..d47adf8 100644 --- a/initial/build.gradle +++ b/initial/build.gradle @@ -9,14 +9,8 @@ buildscript { apply plugin: 'java' apply plugin: 'eclipse' -apply plugin: 'idea' apply plugin: 'spring-boot' -apply plugin: 'application' -run { - args 'sample.txt' -} - jar { baseName = 'gs-uploading-files' version = '0.1.0' @@ -31,8 +25,8 @@ targetCompatibility = 1.8 dependencies { compile("org.springframework.boot:spring-boot-starter-thymeleaf") - compile("org.springframework.boot:spring-boot-starter-hateoas") compile("org.springframework.boot:spring-boot-devtools") - testCompile("junit:junit") + compile("org.springframework.boot:spring-boot-configuration-processor") + testCompile("org.springframework.boot:spring-boot-starter-test") } diff --git a/initial/pom.xml b/initial/pom.xml index d77edf6..2418c0c 100644 --- a/initial/pom.xml +++ b/initial/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework @@ -21,13 +21,20 @@ org.springframework.boot - spring-boot-starter-hateoas + spring-boot-devtools + true org.springframework.boot - spring-boot-devtools + spring-boot-configuration-processor true + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/initial/src/main/java/hello/Application.java b/initial/src/main/java/hello/Application.java index 411450f..d867672 100644 --- a/initial/src/main/java/hello/Application.java +++ b/initial/src/main/java/hello/Application.java @@ -1,8 +1,13 @@ package hello; +import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + } diff --git a/initial/src/main/java/hello/storage/FileSystemStorageService.java b/initial/src/main/java/hello/storage/FileSystemStorageService.java new file mode 100644 index 0000000..775ab71 --- /dev/null +++ b/initial/src/main/java/hello/storage/FileSystemStorageService.java @@ -0,0 +1,86 @@ +package hello.storage; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Service; +import org.springframework.util.FileSystemUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; + +@Service +public class FileSystemStorageService implements StorageService { + + private final Path rootLocation; + + @Autowired + public FileSystemStorageService(StorageProperties properties) { + this.rootLocation = Paths.get(properties.getLocation()); + } + + @Override + public void store(MultipartFile file) { + try { + if (file.isEmpty()) { + throw new StorageException("Failed to store empty file " + file.getOriginalFilename()); + } + Files.copy(file.getInputStream(), this.rootLocation.resolve(file.getOriginalFilename())); + } catch (IOException e) { + throw new StorageException("Failed to store file " + file.getOriginalFilename(), e); + } + } + + @Override + public Stream loadAll() { + try { + return Files.walk(this.rootLocation, 1) + .filter(path -> !path.equals(this.rootLocation)) + .map(path -> this.rootLocation.relativize(path)); + } catch (IOException e) { + throw new StorageException("Failed to read stored files", e); + } + + } + + @Override + public Path load(String filename) { + return rootLocation.resolve(filename); + } + + @Override + public Resource loadAsResource(String filename) { + try { + Path file = load(filename); + Resource resource = new UrlResource(file.toUri()); + if(resource.exists() || resource.isReadable()) { + return resource; + } + else { + throw new StorageFileNotFoundException("Could not read file: " + filename); + + } + } catch (MalformedURLException e) { + throw new StorageFileNotFoundException("Could not read file: " + filename, e); + } + } + + @Override + public void deleteAll() { + FileSystemUtils.deleteRecursively(rootLocation.toFile()); + } + + @Override + public void init() { + try { + Files.createDirectory(rootLocation); + } catch (IOException e) { + throw new StorageException("Could not initialize storage", e); + } + } +} diff --git a/initial/src/main/java/hello/storage/StorageException.java b/initial/src/main/java/hello/storage/StorageException.java new file mode 100644 index 0000000..93fbc36 --- /dev/null +++ b/initial/src/main/java/hello/storage/StorageException.java @@ -0,0 +1,12 @@ +package hello.storage; + +public class StorageException extends RuntimeException { + + public StorageException(String message) { + super(message); + } + + public StorageException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/initial/src/main/java/hello/storage/StorageFileNotFoundException.java b/initial/src/main/java/hello/storage/StorageFileNotFoundException.java new file mode 100644 index 0000000..8cd3799 --- /dev/null +++ b/initial/src/main/java/hello/storage/StorageFileNotFoundException.java @@ -0,0 +1,12 @@ +package hello.storage; + +public class StorageFileNotFoundException extends StorageException { + + public StorageFileNotFoundException(String message) { + super(message); + } + + public StorageFileNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/initial/src/main/java/hello/storage/StorageProperties.java b/initial/src/main/java/hello/storage/StorageProperties.java new file mode 100644 index 0000000..92dfeaa --- /dev/null +++ b/initial/src/main/java/hello/storage/StorageProperties.java @@ -0,0 +1,21 @@ +package hello.storage; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("storage") +public class StorageProperties { + + /** + * Folder location for storing files + */ + private String location = "upload-dir"; + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + +} diff --git a/initial/src/main/java/hello/storage/StorageService.java b/initial/src/main/java/hello/storage/StorageService.java new file mode 100644 index 0000000..44aa4ca --- /dev/null +++ b/initial/src/main/java/hello/storage/StorageService.java @@ -0,0 +1,23 @@ +package hello.storage; + +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; + +import java.nio.file.Path; +import java.util.stream.Stream; + +public interface StorageService { + + void init(); + + void store(MultipartFile file); + + Stream loadAll(); + + Path load(String filename); + + Resource loadAsResource(String filename); + + void deleteAll(); + +} diff --git a/initial/src/main/resources/application.properties b/initial/src/main/resources/application.properties new file mode 100644 index 0000000..e69de29