first issue

This commit is contained in:
mehmet 2026-08-19 11:04:06 +03:00
parent b4dccee2a2
commit 15bbc3f95d
26 changed files with 1056 additions and 15 deletions

50
.gitignore vendored
View File

@ -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

82
apiPdfUploader/pom.xml Normal file
View File

@ -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>

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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("*");
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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> {
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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"
]
}
}

View File

@ -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>

116
webPdfUploader/src/App.css Normal file
View File

@ -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;
}

52
webPdfUploader/src/App.js Normal file
View File

@ -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;

View File

@ -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}`);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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'));