diff --git a/.vscode/launch.json b/.vscode/launch.json index 1c2ee59..ac5d506 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,6 +27,16 @@ "args": "", "envFile": "${workspaceFolder}/.env", "vmArgs": " -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=52355 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dspring.jmx.enabled=true -Djava.rmi.server.hostname=localhost -Dspring.application.admin.enabled=true -Dspring.boot.project.name=resource-service-api" + }, + { + "type": "java", + "name": "Spring Boot-ExplorerRsApplication", + "request": "launch", + "cwd": "${workspaceFolder}", + "mainClass": "ru.molokoin.explorer_rs.ExplorerRsApplication", + "projectName": "explorer_rs", + "args": "", + "envFile": "${workspaceFolder}/.env" } ] } \ No newline at end of file diff --git a/explorer_rs/.env b/explorer_rs/.env new file mode 100644 index 0000000..edf21a4 --- /dev/null +++ b/explorer_rs/.env @@ -0,0 +1 @@ +DATA='/app/explorer_rs/uploads' \ No newline at end of file diff --git a/explorer_rs/docker-compose.yaml b/explorer_rs/docker-compose.yaml index b776190..4789b2c 100644 --- a/explorer_rs/docker-compose.yaml +++ b/explorer_rs/docker-compose.yaml @@ -1,14 +1,19 @@ version: "3.7" services: - resource-service-api: + explorer_rs: build: context: ../explorer_rs dockerfile: dockerfile image: "explorer_rs" - command: ["java","-jar","/app/explorer_rs-0.1.jar"] + command: ["java","-jar","/app/explorer_rs/explorer_rs-0.1.jar"] ports: - 82:8282 restart: unless-stopped + volumes: + - data_explorer_rs:${DATA} +volumes: + data_explorer_rs: + external: true networks: default: external: diff --git a/explorer_rs/dockerfile b/explorer_rs/dockerfile index a93155f..ca16553 100644 --- a/explorer_rs/dockerfile +++ b/explorer_rs/dockerfile @@ -1,8 +1,12 @@ FROM openjdk:17-jdk-alpine RUN apk update RUN apk upgrade -COPY target/explorer_rs-0.1.jar /app/explorer_rs-0.1.jar -WORKDIR /app +COPY target/explorer_rs-0.1.jar /app/explorer_rs/explorer_rs-0.1.jar +WORKDIR /app/explorer_rs # ENTRYPOINT ["java","-jar","/app/resource-service-api-0.1.jar"] # docker image build -t resource-service-api:latest . -# docker run -d -p80:8181 resource-service-api:latest \ No newline at end of file +# docker run -d -p80:8181 resource-service-api:latest + +# Путь к загружаемым на сервер данным: +# /app/explorer_rs/uploads +# Нужно создать для него том, чтобы получить доступ к загружаемым на сервер данным \ No newline at end of file diff --git a/explorer_rs/src/main/java/ru/molokoin/explorer_rs/ExplorerRsApplication.java b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/ExplorerRsApplication.java index 89b9a55..de3ccdc 100644 --- a/explorer_rs/src/main/java/ru/molokoin/explorer_rs/ExplorerRsApplication.java +++ b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/ExplorerRsApplication.java @@ -2,8 +2,12 @@ package ru.molokoin.explorer_rs; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +import ru.molokoin.explorer_rs.config.StorageProperties; @SpringBootApplication +@EnableConfigurationProperties(StorageProperties.class) public class ExplorerRsApplication { public static void main(String[] args) { diff --git a/explorer_rs/src/main/java/ru/molokoin/explorer_rs/config/StorageProperties.java b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/config/StorageProperties.java new file mode 100644 index 0000000..150f6cd --- /dev/null +++ b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/config/StorageProperties.java @@ -0,0 +1,17 @@ +package ru.molokoin.explorer_rs.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "storage") +public class StorageProperties { + private String location; + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + +} diff --git a/explorer_rs/src/main/java/ru/molokoin/explorer_rs/controller/FileController.java b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/controller/FileController.java new file mode 100644 index 0000000..7fb89cc --- /dev/null +++ b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/controller/FileController.java @@ -0,0 +1,73 @@ +package ru.molokoin.explorer_rs.controller; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +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.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import ru.molokoin.explorer_rs.entity.FileResponse; +import ru.molokoin.explorer_rs.repository.StorageService; + +@Controller +public class FileController { + private StorageService storageService; + + public FileController(StorageService storageService) { + this.storageService = storageService; + } + + @GetMapping("/") + public String listAllFiles(Model model) { + model.addAttribute("files", storageService.loadAll().map( + path -> ServletUriComponentsBuilder.fromCurrentContextPath() + .path("/download/") + .path(path.getFileName().toString()) + .toUriString()) + .collect(Collectors.toList())); + return "listFiles"; + } + + @GetMapping("/download/{filename:.+}") + @ResponseBody + public ResponseEntity downloadFile(@PathVariable String filename) { + Resource resource = storageService.loadAsResource(filename); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + resource.getFilename() + "\"") + .body(resource); + } + + @PostMapping("/upload-file") + @ResponseBody + public FileResponse uploadFile(@RequestParam("file") MultipartFile file) { + String name = storageService.store(file); + + String uri = ServletUriComponentsBuilder.fromCurrentContextPath() + .path("/download/") + .path(name) + .toUriString(); + + return new FileResponse(name, uri, file.getContentType(), file.getSize()); + } + + @PostMapping("/upload-multiple-files") + @ResponseBody + public List uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) { + return Arrays.stream(files) + .map(file -> uploadFile(file)) + .collect(Collectors.toList()); + } + +} diff --git a/explorer_rs/src/main/java/ru/molokoin/explorer_rs/entity/FileResponse.java b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/entity/FileResponse.java new file mode 100644 index 0000000..d9c2b90 --- /dev/null +++ b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/entity/FileResponse.java @@ -0,0 +1,18 @@ +package ru.molokoin.explorer_rs.entity; + +import lombok.Data; + +@Data +public class FileResponse { + private String name; + private String uri; + private String type; + private long size; + + public FileResponse(String name, String uri, String type, long size) { + this.name = name; + this.uri = uri; + this.type = type; + this.size = size; + } +} diff --git a/explorer_rs/src/main/java/ru/molokoin/explorer_rs/exception/FileNotFoundException.java b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/exception/FileNotFoundException.java new file mode 100644 index 0000000..8d0c341 --- /dev/null +++ b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/exception/FileNotFoundException.java @@ -0,0 +1,15 @@ +package ru.molokoin.explorer_rs.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class FileNotFoundException extends StorageException{ + public FileNotFoundException(String message) { + super(message); + } + + public FileNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/explorer_rs/src/main/java/ru/molokoin/explorer_rs/exception/StorageException.java b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/exception/StorageException.java new file mode 100644 index 0000000..4e7624a --- /dev/null +++ b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/exception/StorageException.java @@ -0,0 +1,11 @@ +package ru.molokoin.explorer_rs.exception; + +public class StorageException extends RuntimeException{ + public StorageException(String message) { + super(message); + } + + public StorageException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/explorer_rs/src/main/java/ru/molokoin/explorer_rs/repository/FileSystemStorageService.java b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/repository/FileSystemStorageService.java new file mode 100644 index 0000000..4c3a727 --- /dev/null +++ b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/repository/FileSystemStorageService.java @@ -0,0 +1,107 @@ +package ru.molokoin.explorer_rs.repository; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.stream.Stream; + +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.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.annotation.PostConstruct; +import ru.molokoin.explorer_rs.config.StorageProperties; +import ru.molokoin.explorer_rs.exception.StorageException; +import ru.molokoin.explorer_rs.exception.FileNotFoundException; + +@Service +public class FileSystemStorageService implements StorageService{ + private final Path rootLocation; + + @Autowired + public FileSystemStorageService(StorageProperties properties) { + this.rootLocation = Paths.get(properties.getLocation()); + } + + @Override + @PostConstruct + public void init() { + try { + Files.createDirectories(rootLocation); + } catch (IOException e) { + throw new StorageException("Could not initialize storage location", e); + } + } + + @Override + public String store(MultipartFile file) { + String filename = StringUtils.cleanPath(file.getOriginalFilename()); + try { + if (file.isEmpty()) { + throw new StorageException("Failed to store empty file " + filename); + } + if (filename.contains("..")) { + // This is a security check + throw new StorageException( + "Cannot store file with relative path outside current directory " + + filename); + } + try (InputStream inputStream = file.getInputStream()) { + Files.copy(inputStream, this.rootLocation.resolve(filename), + StandardCopyOption.REPLACE_EXISTING); + } + } + catch (IOException e) { + throw new StorageException("Failed to store file " + filename, e); + } + return filename; + } + + @Override + public Stream loadAll() { + try { + return Files.walk(this.rootLocation, 1) + .filter(path -> !path.equals(this.rootLocation)) + .map(this.rootLocation::relativize); + } + 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 FileNotFoundException( + "Could not read file: " + filename); + } + }catch (MalformedURLException e) { + throw new FileNotFoundException("Could not read file: " + filename, e); + } + } + + @Override + public void deleteAll() { + FileSystemUtils.deleteRecursively(rootLocation.toFile()); + } + +} diff --git a/explorer_rs/src/main/java/ru/molokoin/explorer_rs/repository/StorageService.java b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/repository/StorageService.java new file mode 100644 index 0000000..5979353 --- /dev/null +++ b/explorer_rs/src/main/java/ru/molokoin/explorer_rs/repository/StorageService.java @@ -0,0 +1,16 @@ +package ru.molokoin.explorer_rs.repository; + +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(); + String store(MultipartFile file); + Stream loadAll(); + Path load(String filename); + Resource loadAsResource(String filename); + void deleteAll(); +} diff --git a/explorer_rs/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/explorer_rs/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..12f4d28 --- /dev/null +++ b/explorer_rs/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,5 @@ +{"properties": [{ + "name": "storage.location", + "type": "java.lang.String", + "description": "Path to local storage for uploaded files" +}]} \ No newline at end of file diff --git a/explorer_rs/src/main/resources/application.yaml b/explorer_rs/src/main/resources/application.yaml index b5c02f1..84f4beb 100644 --- a/explorer_rs/src/main/resources/application.yaml +++ b/explorer_rs/src/main/resources/application.yaml @@ -11,3 +11,9 @@ spring: url: "jdbc:postgresql://postgres-service:5432/tech-services" username: tech-services password: password + servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB +storage: + location: ./uploads \ No newline at end of file diff --git a/explorer_rs/src/main/resources/templates/listFiles.html b/explorer_rs/src/main/resources/templates/listFiles.html new file mode 100644 index 0000000..a64012d --- /dev/null +++ b/explorer_rs/src/main/resources/templates/listFiles.html @@ -0,0 +1,33 @@ + + + + +

Spring Boot File Upload Example

+ +
+ +

Upload Single File:

+
+

+ +
+ +
+ +

Upload Multiple Files:

+
+

+ +
+ +
+ +

All Uploaded Files:

+
    +
  • + +
  • +
+ + + \ No newline at end of file