Upload plików z użyciem Spring Boot i bazy danych MySQL


Upload plików z użyciem Spring Boot i bazy danych MySQL

W tym wpisie dowiesz się w jaki sposób za pomocą Spring Boota i bazy MySQL zaprojektować aplikację do przechowywania plików w bazie danych. Pliki będą dodawane asynchronicznie czyli bez konieczności przeładowania strony. Wpis bazuje na projekcie – https://www.callicoder.com/spring-boot-file-upload-download-jpa-hibernate-mysql-database-example/ gdzie dodatkowo opisane jest w jaki sposób dodawać wiele plików jednocześnie (paczkami).  Do dzieła – tworzymy nowy projekt Spring Boot – plik pom.xml – niezbędne zależności:

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>
	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
		<scope>runtime</scope>
	</dependency>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.18.10</version>
		<scope>provided</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
		<exclusions>
			<exclusion>
				<groupId>org.junit.vintage</groupId>
				<artifactId>junit-vintage-engine</artifactId>
			</exclusion>
		</exclusions>
	</dependency>
</dependencies>

Konfigurujemy połączenie do bazy danych MySQL oraz reguły dotyczące rozmiaru przesyłanych plików:

spring.datasource.url         = 
spring.datasource.username    = 
spring.datasource.password    = 
spring.jpa.show-sql           = true
spring.jpa.database-platform  = org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto = create

## Hibernate Logging
logging.level.org.hibernate.SQL = DEBUG

## MULTIPART (MultipartProperties)

# Enable multipart uploads
spring.servlet.multipart.enabled = true

# Threshold after which files are written to disk.
spring.servlet.multipart.file-size-threshold = 2KB

# Max file size.
spring.servlet.multipart.max-file-size = 200MB

# Max Request Size
spring.servlet.multipart.max-request-size = 215MB

dla spring boota w wersji 1.5:

spring:
  http:
    multipart:
      max-file-size: 200MB
      max-request-size: 215MB

Warto dodatkowo z poziomu bazy danych MySQL ustawić parametr który zwiększy domyślny rozmiar pakietu przesyłanych danych:

SET GLOBAL max_allowed_packet=1073741824;

Tworzymy model – plik DBFile – korzystając z biblioteki Lombok:

@Entity
@Table(name = "files")
@Getter
@Setter
public class DBFile {

    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;
    private String fileName;
    private String fileType;

    @Lob
    private byte[] data;

    public DBFile() {
    }

    public DBFile(String fileName, String fileType, byte[] data) {
        this.fileName = fileName;
        this.fileType = fileType;
        this.data = data;
    }
}

Model dla odpowiedzi – po przesłaniu pliku:

@Getter
@Setter
@AllArgsConstructor
public class UploadFileResponse {
    private String fileName;
    private String fileDownloadUri;
    private String fileType;
    private long size;
}

Repozytorium do operacji na bazie danych:

@Repository
public interface DBFileRepository extends JpaRepository<DBFile, String> {
}

Teraz cza na serwis:

@Service
public class DBFileStorageService {

    @Autowired
    private DBFileRepository dbFileRepository;

    public DBFile storeFile(MultipartFile file) {
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());

        try {
            if(fileName.contains("..")) {
                throw new FileStorageException("Sorry! Filename contains invalid path sequence " + fileName);
            }

            DBFile dbFile = new DBFile(fileName, file.getContentType(), file.getBytes());

            return dbFileRepository.save(dbFile);
        } catch (IOException ex) {
            throw new FileStorageException("Could not store file " + fileName + ". Please try again!", ex);
        }
    }

    public DBFile getFile(String fileId) {
        return dbFileRepository.findById(fileId)
                .orElseThrow(() -> new MyFileNotFoundException("File not found with id " + fileId));
    }
}

W powyższym serwisie wykorzystywane są dwie klasy wyjątków:

  • Błąd zapisu – FileStorageException:
public class FileStorageException extends RuntimeException {
    public FileStorageException(String message) {
        super(message);
    }
    public FileStorageException(String message, Throwable cause) {
        super(message, cause);
    }
}
  • Plik nie istnieje – MyFileNotFoundException:
@ResponseStatus(HttpStatus.NOT_FOUND)
public class MyFileNotFoundException extends RuntimeException {
    public MyFileNotFoundException(String message) {
        super(message);
    }
    public MyFileNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

RestController w którym definiujemy metody odpowiednio do zapisu i odczytu pliku:

@RestController
public class FileController {

    private static final Logger logger = LoggerFactory.getLogger(FileController.class);

    @Autowired
    private pl.javaleader.fileupload.service.DBFileStorageService DBFileStorageService;

    @PostMapping("/uploadFile")
    public UploadFileResponse uploadFile(@RequestParam("file") MultipartFile file) {

        DBFile dbFile = DBFileStorageService.storeFile(file);

        String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path("/downloadFile/")
                .path(dbFile.getId())
                .toUriString();

        return new UploadFileResponse(dbFile.getFileName(), fileDownloadUri, file.getContentType(), file.getSize());
    }

    @GetMapping("/downloadFile/{fileId}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileId) {
        DBFile dbFile = DBFileStorageService.getFile(fileId);

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(dbFile.getFileType()))
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + dbFile.getFileName() + "\"")
                .body(new ByteArrayResource(dbFile.getData()));
    }
}

Plik JS w którym za pomocą asynchronicznego obiektu JavaScriptXMLHttpRequest dokonywany jest zapis wybranego przez użytkownika pliku. Tworzony jest również adres URL za pomocą którego zapisany plik może zostać pobrany na dysk:

'use strict';

var singleUploadForm        = document.querySelector('#singleUploadForm');
var singleFileUploadInput   = document.querySelector('#singleFileUploadInput');
var singleFileUploadError   = document.querySelector('#singleFileUploadError');
var singleFileUploadSuccess = document.querySelector('#singleFileUploadSuccess');

function uploadSingleFile(file) {

    var formData = new FormData();
    formData.append("file", file);

    var xhr = new XMLHttpRequest();

    xhr.open("POST", "/uploadFile");

    xhr.onload = function() {

        var response = JSON.parse(xhr.responseText);
        if(xhr.status == 200) {

            singleFileUploadError.style.display   = "none";
            singleFileUploadSuccess.innerHTML     = "<p>File uploaded successfully.</p><p>download : <a href='" + response.fileDownloadUri + "' target='_blank'>" + response.fileDownloadUri + "</a></p>";
            singleFileUploadSuccess.style.display = "block";
        } else {
            singleFileUploadSuccess.style.display = "none";
            singleFileUploadError.innerHTML = (response.message) || "an error occurred";
        }
    }

    xhr.send(formData);
}

singleUploadForm.addEventListener('submit', function(event){
    var files = singleFileUploadInput.files;
    if(files.length === 0) {
        singleFileUploadError.innerHTML = "Please select a file";
        singleFileUploadError.style.display = "block";
    }
    uploadSingleFile(files[0]);
    event.preventDefault();
}, true);

Plik widoku – index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
        <title>JavaLeader.pl - upload files using MySQL</title>
        <link rel="stylesheet" href="/css/main.css" />
    </head>
    <body>
        <noscript>
            <h2>JS support error</h2>
        </noscript>
        <div class="upload-container">
            <div class="upload-header">
                <h2>JavaLeader.pl - upload files using MySQL</h2>
            </div>
            <div class="upload-content">
                <div class="single-upload">
                    <h3>Upload Single File</h3>
                    <form id="singleUploadForm" name="singleUploadForm">
                        <input id="singleFileUploadInput" type="file" name="file" class="file-input" required />
                        <button type="submit" class="primary submit-btn">Submit</button>
                    </form>
                    <div class="upload-response">
                        <div id="singleFileUploadError"></div>
                        <div id="singleFileUploadSuccess"></div>
                    </div>
                </div>
            </div>
        </div>
        <script src="/js/main.js" ></script>
    </body>
</html>

Aplikacja po dodaniu pliku stylów:

wynik  z bazy danych:


Leave a comment

Your email address will not be published.


*