first issue
This commit is contained in:
parent
b4dccee2a2
commit
15bbc3f95d
|
|
@ -1,17 +1,9 @@
|
|||
# ---> Java
|
||||
# Compiled class file
|
||||
### Java
|
||||
apiPdfUploader/target/
|
||||
|
||||
# files
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
|
|
@ -20,7 +12,35 @@
|
|||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
#Eclipse Specific
|
||||
.metadata/
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
.project
|
||||
.classpath
|
||||
.factorypath
|
||||
|
||||
|
||||
### NodeJS
|
||||
webPdfUploader/node_modules/
|
||||
webPdfUploader/build/
|
||||
webPdfUploader/package-lock.json
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
|
||||
|
||||
### OS Specific
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
|
||||
### Other
|
||||
fullstackPdfUploader/apiPdfUploader/storage
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.4</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>apiPdfUploader</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>apiPdfFileManager</name>
|
||||
<description>Sample Spring Boot REST API for storing/serving PDF files with MSSQL metadata</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
|
||||
<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>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MS SQL Server JDBC driver -->
|
||||
<dependency>
|
||||
<groupId>com.microsoft.sqlserver</groupId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
-- Reference schema for MS SQL Server.
|
||||
-- Not required to run manually if spring.jpa.hibernate.ddl-auto=update is left enabled,
|
||||
-- but useful for real deployments where DDL is managed separately.
|
||||
|
||||
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'pdf_file_db')
|
||||
BEGIN
|
||||
CREATE DATABASE pdf_file_db;
|
||||
END
|
||||
GO
|
||||
|
||||
USE pdf_file_db;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'file_document')
|
||||
BEGIN
|
||||
CREATE TABLE file_document (
|
||||
id BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||
original_file_name NVARCHAR(255) NOT NULL,
|
||||
stored_file_name NVARCHAR(255) NOT NULL,
|
||||
file_path NVARCHAR(1000) NOT NULL,
|
||||
content_type NVARCHAR(100) NULL,
|
||||
file_size BIGINT NOT NULL DEFAULT 0,
|
||||
upload_date DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.example.pdfservice;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
public class PdfServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(PdfServiceApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.example.pdfservice.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Reads "app.file.storage-dir" from application.properties.
|
||||
* This is the ROOT directory on disk where uploaded PDFs are physically kept.
|
||||
* The full path of each individual file is what gets persisted in the DB.
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "app.file")
|
||||
public class FileStorageProperties {
|
||||
|
||||
private String storageDir;
|
||||
|
||||
public String getStorageDir() {
|
||||
return storageDir;
|
||||
}
|
||||
|
||||
public void setStorageDir(String storageDir) {
|
||||
this.storageDir = storageDir;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.example.pdfservice.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Allows the React dev server (http://localhost:3000) to call this API
|
||||
* during local development. Tighten this for production deployments.
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins("http://localhost:3000")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.example.pdfservice.controller;
|
||||
|
||||
import com.example.pdfservice.dto.FileResponse;
|
||||
import com.example.pdfservice.entity.FileDocument;
|
||||
import com.example.pdfservice.service.FileStorageService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/files")
|
||||
public class FileController {
|
||||
|
||||
private final FileStorageService fileStorageService;
|
||||
|
||||
public FileController(FileStorageService fileStorageService) {
|
||||
this.fileStorageService = fileStorageService;
|
||||
}
|
||||
|
||||
/** 6. List all files */
|
||||
@GetMapping
|
||||
public ResponseEntity<List<FileResponse>> listFiles() {
|
||||
return ResponseEntity.ok(fileStorageService.listAll());
|
||||
}
|
||||
|
||||
/** 7. Upload a new PDF file (multipart/form-data, field name "file") */
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<FileResponse> uploadFile(@RequestParam("file") MultipartFile file) {
|
||||
FileResponse response = fileStorageService.store(file);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/** 7. Download a file as an attachment */
|
||||
@GetMapping("/{id}/download")
|
||||
public ResponseEntity<Resource> downloadFile(@PathVariable Long id) {
|
||||
FileDocument metadata = fileStorageService.getMetadata(id);
|
||||
Resource resource = fileStorageService.loadAsResource(id);
|
||||
|
||||
String encodedName = URLEncoder.encode(metadata.getOriginalFileName(), StandardCharsets.UTF_8);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"" + metadata.getOriginalFileName() + "\"; filename*=UTF-8''" + encodedName)
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
/** 8. Preview a file inline in the browser (e.g. inside an <iframe> or <embed>) */
|
||||
@GetMapping("/{id}/preview")
|
||||
public ResponseEntity<Resource> previewFile(@PathVariable Long id) {
|
||||
FileDocument metadata = fileStorageService.getMetadata(id);
|
||||
Resource resource = fileStorageService.loadAsResource(id);
|
||||
|
||||
String encodedName = URLEncoder.encode(metadata.getOriginalFileName(), StandardCharsets.UTF_8);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"inline; filename=\"" + metadata.getOriginalFileName() + "\"; filename*=UTF-8''" + encodedName)
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
/** Optional: delete a file */
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteFile(@PathVariable Long id) {
|
||||
fileStorageService.delete(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.example.pdfservice.dto;
|
||||
|
||||
import com.example.pdfservice.entity.FileDocument;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* What we expose to the React app. Note we never expose the raw disk path
|
||||
* to the client — only an id it can use to call the download/preview endpoints.
|
||||
*/
|
||||
public class FileResponse {
|
||||
|
||||
private Long id;
|
||||
private String fileName;
|
||||
private String contentType;
|
||||
private long fileSize;
|
||||
private LocalDateTime uploadDate;
|
||||
private String downloadUrl;
|
||||
private String previewUrl;
|
||||
|
||||
public static FileResponse fromEntity(FileDocument doc) {
|
||||
FileResponse dto = new FileResponse();
|
||||
dto.id = doc.getId();
|
||||
dto.fileName = doc.getOriginalFileName();
|
||||
dto.contentType = doc.getContentType();
|
||||
dto.fileSize = doc.getFileSize();
|
||||
dto.uploadDate = doc.getUploadDate();
|
||||
dto.downloadUrl = "/api/files/" + doc.getId() + "/download";
|
||||
dto.previewUrl = "/api/files/" + doc.getId() + "/preview";
|
||||
return dto;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public long getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public LocalDateTime getUploadDate() {
|
||||
return uploadDate;
|
||||
}
|
||||
|
||||
public String getDownloadUrl() {
|
||||
return downloadUrl;
|
||||
}
|
||||
|
||||
public String getPreviewUrl() {
|
||||
return previewUrl;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.example.pdfservice.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Metadata row for a single stored PDF.
|
||||
* The actual bytes live on disk at "filePath" — only the path is kept in SQL Server.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "file_document")
|
||||
public class FileDocument {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/** Original name of the file as uploaded by the user, e.g. "invoice.pdf" */
|
||||
@Column(name = "original_file_name", nullable = false, length = 255)
|
||||
private String originalFileName;
|
||||
|
||||
/** Randomized name used on disk to avoid collisions, e.g. "3f2a...-invoice.pdf" */
|
||||
@Column(name = "stored_file_name", nullable = false, length = 255)
|
||||
private String storedFileName;
|
||||
|
||||
/** Full absolute path on disk where the file is stored */
|
||||
@Column(name = "file_path", nullable = false, length = 1000)
|
||||
private String filePath;
|
||||
|
||||
@Column(name = "content_type", length = 100)
|
||||
private String contentType;
|
||||
|
||||
@Column(name = "file_size")
|
||||
private long fileSize;
|
||||
|
||||
@Column(name = "upload_date", nullable = false)
|
||||
private LocalDateTime uploadDate;
|
||||
|
||||
public FileDocument() {
|
||||
}
|
||||
|
||||
public FileDocument(String originalFileName, String storedFileName, String filePath,
|
||||
String contentType, long fileSize, LocalDateTime uploadDate) {
|
||||
this.originalFileName = originalFileName;
|
||||
this.storedFileName = storedFileName;
|
||||
this.filePath = filePath;
|
||||
this.contentType = contentType;
|
||||
this.fileSize = fileSize;
|
||||
this.uploadDate = uploadDate;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getOriginalFileName() {
|
||||
return originalFileName;
|
||||
}
|
||||
|
||||
public void setOriginalFileName(String originalFileName) {
|
||||
this.originalFileName = originalFileName;
|
||||
}
|
||||
|
||||
public String getStoredFileName() {
|
||||
return storedFileName;
|
||||
}
|
||||
|
||||
public void setStoredFileName(String storedFileName) {
|
||||
this.storedFileName = storedFileName;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public long getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public void setFileSize(long fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public LocalDateTime getUploadDate() {
|
||||
return uploadDate;
|
||||
}
|
||||
|
||||
public void setUploadDate(LocalDateTime uploadDate) {
|
||||
this.uploadDate = uploadDate;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.example.pdfservice.exception;
|
||||
|
||||
public class FileNotFoundException extends RuntimeException {
|
||||
|
||||
public FileNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FileNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.example.pdfservice.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(FileNotFoundException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleNotFound(FileNotFoundException ex) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body(HttpStatus.NOT_FOUND, ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleTooLarge(MaxUploadSizeExceededException ex) {
|
||||
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
|
||||
.body(body(HttpStatus.PAYLOAD_TOO_LARGE, "Uploaded file is too large"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleBadRequest(IllegalArgumentException ex) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body(HttpStatus.BAD_REQUEST, ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Map<String, Object>> handleGeneric(Exception ex) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(body(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error: " + ex.getMessage()));
|
||||
}
|
||||
|
||||
private Map<String, Object> body(HttpStatus status, String message) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("timestamp", LocalDateTime.now());
|
||||
map.put("status", status.value());
|
||||
map.put("error", status.getReasonPhrase());
|
||||
map.put("message", message);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.example.pdfservice.repository;
|
||||
|
||||
import com.example.pdfservice.entity.FileDocument;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface FileDocumentRepository extends JpaRepository<FileDocument, Long> {
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.example.pdfservice.service;
|
||||
|
||||
import com.example.pdfservice.config.FileStorageProperties;
|
||||
import com.example.pdfservice.dto.FileResponse;
|
||||
import com.example.pdfservice.entity.FileDocument;
|
||||
import com.example.pdfservice.exception.FileNotFoundException;
|
||||
import com.example.pdfservice.repository.FileDocumentRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.nio.file.StandardCopyOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class FileStorageService {
|
||||
|
||||
private final FileDocumentRepository repository;
|
||||
private final FileStorageProperties properties;
|
||||
private Path rootLocation;
|
||||
|
||||
public FileStorageService(FileDocumentRepository repository, FileStorageProperties properties) {
|
||||
this.repository = repository;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
this.rootLocation = Paths.get(properties.getStorageDir()).toAbsolutePath().normalize();
|
||||
Files.createDirectories(this.rootLocation);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Could not initialize storage directory: " + properties.getStorageDir(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public FileResponse store(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("Cannot upload an empty file");
|
||||
}
|
||||
|
||||
String originalFileName = Path.of(file.getOriginalFilename()).getFileName().toString();
|
||||
|
||||
if (!originalFileName.toLowerCase().endsWith(".pdf")) {
|
||||
throw new IllegalArgumentException("Only PDF files are allowed");
|
||||
}
|
||||
|
||||
String storedFileName = UUID.randomUUID() + "-" + originalFileName;
|
||||
|
||||
try {
|
||||
Path destination = rootLocation.resolve(storedFileName).normalize();
|
||||
|
||||
// Guard against path traversal
|
||||
if (!destination.getParent().equals(rootLocation)) {
|
||||
throw new IllegalArgumentException("Invalid file path");
|
||||
}
|
||||
|
||||
Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
FileDocument doc = new FileDocument(
|
||||
originalFileName,
|
||||
storedFileName,
|
||||
destination.toString(),
|
||||
file.getContentType() != null ? file.getContentType() : "application/pdf",
|
||||
file.getSize(),
|
||||
LocalDateTime.now()
|
||||
);
|
||||
|
||||
FileDocument saved = repository.save(doc);
|
||||
return FileResponse.fromEntity(saved);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to store file " + originalFileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<FileResponse> listAll() {
|
||||
return repository.findAll().stream()
|
||||
.map(FileResponse::fromEntity)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public FileDocument getMetadata(Long id) {
|
||||
return repository.findById(id)
|
||||
.orElseThrow(() -> new FileNotFoundException("No file found with id " + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the actual PDF bytes from disk using the path stored in the DB.
|
||||
*/
|
||||
public Resource loadAsResource(Long id) {
|
||||
FileDocument doc = getMetadata(id);
|
||||
try {
|
||||
Path filePath = Paths.get(doc.getFilePath()).normalize();
|
||||
Resource resource = new UrlResource(filePath.toUri());
|
||||
|
||||
if (!resource.exists() || !resource.isReadable()) {
|
||||
throw new FileNotFoundException("File not readable on disk: " + doc.getFilePath());
|
||||
}
|
||||
return resource;
|
||||
} catch (MalformedURLException e) {
|
||||
throw new FileNotFoundException("Could not read file with id " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
FileDocument doc = getMetadata(id);
|
||||
try {
|
||||
Files.deleteIfExists(Paths.get(doc.getFilePath()));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to delete file from disk", e);
|
||||
}
|
||||
repository.delete(doc);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# ===== Server =====
|
||||
server.port=8080
|
||||
|
||||
# ===== MS SQL Server datasource =====
|
||||
# Update host, port, databaseName, username, password for your environment.
|
||||
spring.datasource.url=jdbc:sqlserver://10.60.5.120:1433;databaseName=test_pdf;encrypt=true;trustServerCertificate=true
|
||||
spring.datasource.username=app
|
||||
spring.datasource.password=gast!Mam!25
|
||||
spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
|
||||
# ===== JPA / Hibernate =====
|
||||
spring.jpa.database-platform=org.hibernate.dialect.SQLServerDialect
|
||||
# "update" auto-creates/updates the table for this sample. Use "validate" or migrations (Flyway/Liquibase) in real projects.
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.jpa.show-sql=false
|
||||
spring.jpa.properties.hibernate.format_sql=true
|
||||
|
||||
# ===== File upload limits =====
|
||||
spring.servlet.multipart.max-file-size=30MB
|
||||
spring.servlet.multipart.max-request-size=30MB
|
||||
|
||||
# ===== Where PDF files are physically stored on disk =====
|
||||
# The FULL PATH of each uploaded file (this directory + generated file name) is what gets saved in the DB.
|
||||
app.file.storage-dir=./storage/pdf-files
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "pdf-file-client",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"axios": "^1.19.0",
|
||||
"react": "16.8.0",
|
||||
"react-dom": "16.8.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"ajv": "^6.12.6",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>PDF File Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f4f6f8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
max-width: 900px;
|
||||
margin: 40px auto;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
border: 1px dashed #c0c7d0;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.upload-box h3 {
|
||||
width: 100%;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
background: #2f6fed;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #9db3e8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: #d64545;
|
||||
}
|
||||
|
||||
.file-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.file-table th,
|
||||
.file-table td {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eaeef2;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #d64545;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.preview-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.preview-modal {
|
||||
background: #fff;
|
||||
width: 80vw;
|
||||
height: 85vh;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #eaeef2;
|
||||
}
|
||||
|
||||
.preview-object {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import FileList from './components/FileList';
|
||||
import FileUpload from './components/FileUpload';
|
||||
import FilePreview from './components/FilePreview';
|
||||
import { fetchFiles, deleteFile } from './api/fileApi';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [previewFile, setPreviewFile] = useState(null);
|
||||
|
||||
const loadFiles = useCallback(() => {
|
||||
setLoading(true);
|
||||
fetchFiles()
|
||||
.then(data => {
|
||||
setFiles(data);
|
||||
setError(null);
|
||||
})
|
||||
.catch(() => setError('Could not load files. Is the backend running?'))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadFiles();
|
||||
}, [loadFiles]);
|
||||
|
||||
const handleDelete = id => {
|
||||
if (!window.confirm('Delete this file?')) return;
|
||||
deleteFile(id).then(loadFiles);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<h1>PDF File Manager</h1>
|
||||
|
||||
<FileUpload onUploadSuccess={loadFiles} />
|
||||
|
||||
<h3>Stored files</h3>
|
||||
{loading && <p>Loading...</p>}
|
||||
{error && <p className="error-text">{error}</p>}
|
||||
{!loading && !error && (
|
||||
<FileList files={files} onPreview={setPreviewFile} onDelete={handleDelete} />
|
||||
)}
|
||||
|
||||
<FilePreview file={previewFile} onClose={() => setPreviewFile(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import axios from 'axios';
|
||||
|
||||
// Point this at your Spring Boot backend
|
||||
const API_BASE_URL = 'http://localhost:8080/api/files';
|
||||
|
||||
export function fetchFiles() {
|
||||
return axios.get(API_BASE_URL).then(res => res.data);
|
||||
}
|
||||
|
||||
export function uploadFile(file, onUploadProgress) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
return axios
|
||||
.post(API_BASE_URL, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
onUploadProgress,
|
||||
})
|
||||
.then(res => res.data);
|
||||
}
|
||||
|
||||
export function getDownloadUrl(id) {
|
||||
return `${API_BASE_URL}/${id}/download`;
|
||||
}
|
||||
|
||||
export function getPreviewUrl(id) {
|
||||
return `${API_BASE_URL}/${id}/preview`;
|
||||
}
|
||||
|
||||
export function deleteFile(id) {
|
||||
return axios.delete(`${API_BASE_URL}/${id}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
import { getDownloadUrl } from '../api/fileApi';
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
||||
}
|
||||
|
||||
function formatDate(isoString) {
|
||||
if (!isoString) return '';
|
||||
const d = new Date(isoString);
|
||||
return d.toLocaleString();
|
||||
}
|
||||
|
||||
function FileList({ files, onPreview, onDelete }) {
|
||||
if (!files || files.length === 0) {
|
||||
return <p className="empty-state">No files uploaded yet.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="file-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File name</th>
|
||||
<th>Size</th>
|
||||
<th>Uploaded</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map(file => (
|
||||
<tr key={file.id}>
|
||||
<td>{file.fileName}</td>
|
||||
<td>{formatBytes(file.fileSize)}</td>
|
||||
<td>{formatDate(file.uploadDate)}</td>
|
||||
<td className="actions-cell">
|
||||
<button onClick={() => onPreview(file)}>Preview</button>
|
||||
<a href={getDownloadUrl(file.id)}>
|
||||
<button>Download</button>
|
||||
</a>
|
||||
<button className="danger" onClick={() => onDelete(file.id)}>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileList;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { getPreviewUrl } from '../api/fileApi';
|
||||
|
||||
function FilePreview({ file, onClose }) {
|
||||
if (!file) return null;
|
||||
|
||||
const previewUrl = getPreviewUrl(file.id);
|
||||
|
||||
return (
|
||||
<div className="preview-overlay" onClick={onClose}>
|
||||
<div className="preview-modal" onClick={e => e.stopPropagation()}>
|
||||
<div className="preview-header">
|
||||
<span>{file.fileName}</span>
|
||||
<button onClick={onClose}>Close</button>
|
||||
</div>
|
||||
{/* Browsers with a built-in PDF viewer will render this inline */}
|
||||
<object data={previewUrl} type="application/pdf" className="preview-object">
|
||||
<p>
|
||||
Your browser can't display PDFs inline.{' '}
|
||||
<a href={previewUrl} target="_blank" rel="noopener noreferrer">
|
||||
Open the PDF in a new tab
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</object>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilePreview;
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React, { useState } from 'react';
|
||||
import { uploadFile } from '../api/fileApi';
|
||||
|
||||
function FileUpload({ onUploadSuccess }) {
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleFileChange = event => {
|
||||
const file = event.target.files[0];
|
||||
setError(null);
|
||||
if (file && file.type !== 'application/pdf') {
|
||||
setError('Only PDF files are allowed.');
|
||||
setSelectedFile(null);
|
||||
return;
|
||||
}
|
||||
setSelectedFile(file);
|
||||
};
|
||||
|
||||
const handleUpload = () => {
|
||||
if (!selectedFile) {
|
||||
setError('Please choose a PDF file first.');
|
||||
return;
|
||||
}
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
|
||||
uploadFile(selectedFile, progressEvent => {
|
||||
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
setProgress(percent);
|
||||
})
|
||||
.then(() => {
|
||||
setSelectedFile(null);
|
||||
setProgress(0);
|
||||
setUploading(false);
|
||||
onUploadSuccess();
|
||||
})
|
||||
.catch(err => {
|
||||
setUploading(false);
|
||||
setError(
|
||||
(err.response && err.response.data && err.response.data.message) ||
|
||||
'Upload failed. Please try again.'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="upload-box">
|
||||
<h3>Upload a PDF</h3>
|
||||
<input type="file" accept="application/pdf" onChange={handleFileChange} disabled={uploading} />
|
||||
<button onClick={handleUpload} disabled={uploading || !selectedFile}>
|
||||
{uploading ? `Uploading... ${progress}%` : 'Upload'}
|
||||
</button>
|
||||
{error && <div className="error-text">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileUpload;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './App.css';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
Loading…
Reference in New Issue