diff --git a/.gitignore b/.gitignore index 9154f4c..2ff1fa2 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/apiPdfUploader/pom.xml b/apiPdfUploader/pom.xml new file mode 100644 index 0000000..cbfdab6 --- /dev/null +++ b/apiPdfUploader/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.4 + + + + com.example + apiPdfUploader + 1.0.0 + apiPdfFileManager + Sample Spring Boot REST API for storing/serving PDF files with MSSQL metadata + + + 21 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.microsoft.sqlserver + mssql-jdbc + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/apiPdfUploader/schema-mssql.sql b/apiPdfUploader/schema-mssql.sql new file mode 100644 index 0000000..8ff8824 --- /dev/null +++ b/apiPdfUploader/schema-mssql.sql @@ -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 diff --git a/apiPdfUploader/src/main/java/com/example/pdfservice/PdfServiceApplication.java b/apiPdfUploader/src/main/java/com/example/pdfservice/PdfServiceApplication.java new file mode 100644 index 0000000..772909e --- /dev/null +++ b/apiPdfUploader/src/main/java/com/example/pdfservice/PdfServiceApplication.java @@ -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); + } + +} diff --git a/apiPdfUploader/src/main/java/com/example/pdfservice/config/FileStorageProperties.java b/apiPdfUploader/src/main/java/com/example/pdfservice/config/FileStorageProperties.java new file mode 100644 index 0000000..668a1af --- /dev/null +++ b/apiPdfUploader/src/main/java/com/example/pdfservice/config/FileStorageProperties.java @@ -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; + } +} diff --git a/apiPdfUploader/src/main/java/com/example/pdfservice/config/WebConfig.java b/apiPdfUploader/src/main/java/com/example/pdfservice/config/WebConfig.java new file mode 100644 index 0000000..7af319c --- /dev/null +++ b/apiPdfUploader/src/main/java/com/example/pdfservice/config/WebConfig.java @@ -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("*"); + } +} diff --git a/apiPdfUploader/src/main/java/com/example/pdfservice/controller/FileController.java b/apiPdfUploader/src/main/java/com/example/pdfservice/controller/FileController.java new file mode 100644 index 0000000..3b06562 --- /dev/null +++ b/apiPdfUploader/src/main/java/com/example/pdfservice/controller/FileController.java @@ -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> 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 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 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 or ) */ + @GetMapping("/{id}/preview") + public ResponseEntity 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 deleteFile(@PathVariable Long id) { + fileStorageService.delete(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/apiPdfUploader/src/main/java/com/example/pdfservice/dto/FileResponse.java b/apiPdfUploader/src/main/java/com/example/pdfservice/dto/FileResponse.java new file mode 100644 index 0000000..2aca340 --- /dev/null +++ b/apiPdfUploader/src/main/java/com/example/pdfservice/dto/FileResponse.java @@ -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; + } +} diff --git a/apiPdfUploader/src/main/java/com/example/pdfservice/entity/FileDocument.java b/apiPdfUploader/src/main/java/com/example/pdfservice/entity/FileDocument.java new file mode 100644 index 0000000..b2ef154 --- /dev/null +++ b/apiPdfUploader/src/main/java/com/example/pdfservice/entity/FileDocument.java @@ -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; + } +} diff --git a/apiPdfUploader/src/main/java/com/example/pdfservice/exception/FileNotFoundException.java b/apiPdfUploader/src/main/java/com/example/pdfservice/exception/FileNotFoundException.java new file mode 100644 index 0000000..b7963e1 --- /dev/null +++ b/apiPdfUploader/src/main/java/com/example/pdfservice/exception/FileNotFoundException.java @@ -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); + } +} diff --git a/apiPdfUploader/src/main/java/com/example/pdfservice/exception/GlobalExceptionHandler.java b/apiPdfUploader/src/main/java/com/example/pdfservice/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a79c80f --- /dev/null +++ b/apiPdfUploader/src/main/java/com/example/pdfservice/exception/GlobalExceptionHandler.java @@ -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> handleNotFound(FileNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body(HttpStatus.NOT_FOUND, ex.getMessage())); + } + + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResponseEntity> 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> handleBadRequest(IllegalArgumentException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body(HttpStatus.BAD_REQUEST, ex.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGeneric(Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(body(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error: " + ex.getMessage())); + } + + private Map body(HttpStatus status, String message) { + Map map = new LinkedHashMap<>(); + map.put("timestamp", LocalDateTime.now()); + map.put("status", status.value()); + map.put("error", status.getReasonPhrase()); + map.put("message", message); + return map; + } +} diff --git a/apiPdfUploader/src/main/java/com/example/pdfservice/repository/FileDocumentRepository.java b/apiPdfUploader/src/main/java/com/example/pdfservice/repository/FileDocumentRepository.java new file mode 100644 index 0000000..cdcfb7d --- /dev/null +++ b/apiPdfUploader/src/main/java/com/example/pdfservice/repository/FileDocumentRepository.java @@ -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 { +} diff --git a/apiPdfUploader/src/main/java/com/example/pdfservice/service/FileStorageService.java b/apiPdfUploader/src/main/java/com/example/pdfservice/service/FileStorageService.java new file mode 100644 index 0000000..684dcc8 --- /dev/null +++ b/apiPdfUploader/src/main/java/com/example/pdfservice/service/FileStorageService.java @@ -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 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); + } +} diff --git a/apiPdfUploader/src/main/resources/application.properties b/apiPdfUploader/src/main/resources/application.properties new file mode 100644 index 0000000..44a2c4b --- /dev/null +++ b/apiPdfUploader/src/main/resources/application.properties @@ -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 diff --git a/apiPdfUploader/storage/pdf-files/40cedaf7-9ee1-4528-b909-cb08298fe238-01 Ekler KAAN P1S2 ExPS .pdf b/apiPdfUploader/storage/pdf-files/40cedaf7-9ee1-4528-b909-cb08298fe238-01 Ekler KAAN P1S2 ExPS .pdf new file mode 100644 index 0000000..3ea7c72 Binary files /dev/null and b/apiPdfUploader/storage/pdf-files/40cedaf7-9ee1-4528-b909-cb08298fe238-01 Ekler KAAN P1S2 ExPS .pdf differ diff --git a/apiPdfUploader/storage/pdf-files/5cd7fae0-ab4d-4a62-9e3a-8be29452f7f3-ExPS_CMP_REV 7.0.pdf b/apiPdfUploader/storage/pdf-files/5cd7fae0-ab4d-4a62-9e3a-8be29452f7f3-ExPS_CMP_REV 7.0.pdf new file mode 100644 index 0000000..f152254 Binary files /dev/null and b/apiPdfUploader/storage/pdf-files/5cd7fae0-ab4d-4a62-9e3a-8be29452f7f3-ExPS_CMP_REV 7.0.pdf differ diff --git a/apiPdfUploader/storage/pdf-files/99344888-76e0-4f40-89f3-6f1ad9930761-2420_260805113124_001.pdf b/apiPdfUploader/storage/pdf-files/99344888-76e0-4f40-89f3-6f1ad9930761-2420_260805113124_001.pdf new file mode 100644 index 0000000..4cfdca2 Binary files /dev/null and b/apiPdfUploader/storage/pdf-files/99344888-76e0-4f40-89f3-6f1ad9930761-2420_260805113124_001.pdf differ diff --git a/webPdfUploader/package.json b/webPdfUploader/package.json new file mode 100644 index 0000000..76751e7 --- /dev/null +++ b/webPdfUploader/package.json @@ -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" + ] + } +} diff --git a/webPdfUploader/public/index.html b/webPdfUploader/public/index.html new file mode 100644 index 0000000..cb67e7d --- /dev/null +++ b/webPdfUploader/public/index.html @@ -0,0 +1,12 @@ + + + + + + PDF File Manager + + + You need to enable JavaScript to run this app. + + + diff --git a/webPdfUploader/src/App.css b/webPdfUploader/src/App.css new file mode 100644 index 0000000..e6b5140 --- /dev/null +++ b/webPdfUploader/src/App.css @@ -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; +} diff --git a/webPdfUploader/src/App.js b/webPdfUploader/src/App.js new file mode 100644 index 0000000..08cf2fa --- /dev/null +++ b/webPdfUploader/src/App.js @@ -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 ( + + PDF File Manager + + + + Stored files + {loading && Loading...} + {error && {error}} + {!loading && !error && ( + + )} + + setPreviewFile(null)} /> + + ); +} + +export default App; diff --git a/webPdfUploader/src/api/fileApi.js b/webPdfUploader/src/api/fileApi.js new file mode 100644 index 0000000..4952243 --- /dev/null +++ b/webPdfUploader/src/api/fileApi.js @@ -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}`); +} diff --git a/webPdfUploader/src/components/FileList.js b/webPdfUploader/src/components/FileList.js new file mode 100644 index 0000000..98f8422 --- /dev/null +++ b/webPdfUploader/src/components/FileList.js @@ -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 No files uploaded yet.; + } + + return ( + + + + File name + Size + Uploaded + Actions + + + + {files.map(file => ( + + {file.fileName} + {formatBytes(file.fileSize)} + {formatDate(file.uploadDate)} + + onPreview(file)}>Preview + + Download + + onDelete(file.id)}> + Delete + + + + ))} + + + ); +} + +export default FileList; diff --git a/webPdfUploader/src/components/FilePreview.js b/webPdfUploader/src/components/FilePreview.js new file mode 100644 index 0000000..b4c60f2 --- /dev/null +++ b/webPdfUploader/src/components/FilePreview.js @@ -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 ( + + e.stopPropagation()}> + + {file.fileName} + Close + + {/* Browsers with a built-in PDF viewer will render this inline */} + + + Your browser can't display PDFs inline.{' '} + + Open the PDF in a new tab + + . + + + + + ); +} + +export default FilePreview; diff --git a/webPdfUploader/src/components/FileUpload.js b/webPdfUploader/src/components/FileUpload.js new file mode 100644 index 0000000..bd13292 --- /dev/null +++ b/webPdfUploader/src/components/FileUpload.js @@ -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 ( + + Upload a PDF + + + {uploading ? `Uploading... ${progress}%` : 'Upload'} + + {error && {error}} + + ); +} + +export default FileUpload; diff --git a/webPdfUploader/src/index.js b/webPdfUploader/src/index.js new file mode 100644 index 0000000..39b6f41 --- /dev/null +++ b/webPdfUploader/src/index.js @@ -0,0 +1,6 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './App.css'; +import App from './App'; + +ReactDOM.render(, document.getElementById('root'));
Loading...
{error}
No files uploaded yet.
+ Your browser can't display PDFs inline.{' '} + + Open the PDF in a new tab + + . +