esoe
6 months ago
15 changed files with 330 additions and 5 deletions
@ -1,8 +1,12 @@ |
|||||||
FROM openjdk:17-jdk-alpine |
FROM openjdk:17-jdk-alpine |
||||||
RUN apk update |
RUN apk update |
||||||
RUN apk upgrade |
RUN apk upgrade |
||||||
COPY target/explorer_rs-0.1.jar /app/explorer_rs-0.1.jar |
COPY target/explorer_rs-0.1.jar /app/explorer_rs/explorer_rs-0.1.jar |
||||||
WORKDIR /app |
WORKDIR /app/explorer_rs |
||||||
# ENTRYPOINT ["java","-jar","/app/resource-service-api-0.1.jar"] |
# ENTRYPOINT ["java","-jar","/app/resource-service-api-0.1.jar"] |
||||||
# docker image build -t resource-service-api:latest . |
# docker image build -t resource-service-api:latest . |
||||||
# docker run -d -p80:8181 resource-service-api:latest |
# docker run -d -p80:8181 resource-service-api:latest |
||||||
|
|
||||||
|
# Путь к загружаемым на сервер данным: |
||||||
|
# /app/explorer_rs/uploads |
||||||
|
# Нужно создать для него том, чтобы получить доступ к загружаемым на сервер данным |
@ -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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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<Resource> 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<FileResponse> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) { |
||||||
|
return Arrays.stream(files) |
||||||
|
.map(file -> uploadFile(file)) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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<Path> 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()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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<Path> loadAll(); |
||||||
|
Path load(String filename); |
||||||
|
Resource loadAsResource(String filename); |
||||||
|
void deleteAll(); |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
{"properties": [{ |
||||||
|
"name": "storage.location", |
||||||
|
"type": "java.lang.String", |
||||||
|
"description": "Path to local storage for uploaded files" |
||||||
|
}]} |
@ -0,0 +1,33 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org"> |
||||||
|
<body> |
||||||
|
|
||||||
|
<h1>Spring Boot File Upload Example</h1> |
||||||
|
|
||||||
|
<hr/> |
||||||
|
|
||||||
|
<h4>Upload Single File:</h4> |
||||||
|
<form method="POST" enctype="multipart/form-data" th:action="@{/upload-file}"> |
||||||
|
<input type="file" name="file"> <br/><br/> |
||||||
|
<button type="submit">Submit</button> |
||||||
|
</form> |
||||||
|
|
||||||
|
<hr/> |
||||||
|
|
||||||
|
<h4>Upload Multiple Files:</h4> |
||||||
|
<form method="POST" enctype="multipart/form-data" th:action="@{/upload-multiple-files}"> |
||||||
|
<input type="file" name="files" multiple> <br/><br/> |
||||||
|
<button type="submit">Submit</button> |
||||||
|
</form> |
||||||
|
|
||||||
|
<hr/> |
||||||
|
|
||||||
|
<h2>All Uploaded Files:</h2> |
||||||
|
<ul> |
||||||
|
<li th:each="file : ${files}"> |
||||||
|
<a th:href="${file}" target="_blank" th:text="${file}"></a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
</body> |
||||||
|
</html> |
Loading…
Reference in new issue