first issue
This commit is contained in:
parent
b4dccee2a2
commit
15bbc3f95d
|
|
@ -1,17 +1,9 @@
|
||||||
# ---> Java
|
### Java
|
||||||
# Compiled class file
|
apiPdfUploader/target/
|
||||||
|
|
||||||
|
# files
|
||||||
*.class
|
*.class
|
||||||
|
|
||||||
# Log file
|
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# BlueJ files
|
|
||||||
*.ctxt
|
|
||||||
|
|
||||||
# Mobile Tools for Java (J2ME)
|
|
||||||
.mtj.tmp/
|
|
||||||
|
|
||||||
# Package Files #
|
|
||||||
*.jar
|
*.jar
|
||||||
*.war
|
*.war
|
||||||
*.nar
|
*.nar
|
||||||
|
|
@ -20,7 +12,35 @@
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
#Eclipse Specific
|
||||||
hs_err_pid*
|
.metadata/
|
||||||
replay_pid*
|
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