feat(back-end):improvement
This commit is contained in:
@@ -26,13 +26,14 @@ public class CustomQuoteRequestController {
|
||||
private final CustomQuoteRequestRepository requestRepo;
|
||||
private final CustomQuoteRequestAttachmentRepository attachmentRepo;
|
||||
|
||||
// TODO: Inject Storage Service
|
||||
private static final String STORAGE_ROOT = "storage_requests";
|
||||
private final com.printcalculator.service.StorageService storageService;
|
||||
|
||||
public CustomQuoteRequestController(CustomQuoteRequestRepository requestRepo,
|
||||
CustomQuoteRequestAttachmentRepository attachmentRepo) {
|
||||
CustomQuoteRequestAttachmentRepository attachmentRepo,
|
||||
com.printcalculator.service.StorageService storageService) {
|
||||
this.requestRepo = requestRepo;
|
||||
this.attachmentRepo = attachmentRepo;
|
||||
this.storageService = storageService;
|
||||
}
|
||||
|
||||
// 1. Create Custom Quote Request
|
||||
@@ -91,10 +92,8 @@ public class CustomQuoteRequestController {
|
||||
attachment.setStoredRelativePath(relativePath);
|
||||
attachmentRepo.save(attachment);
|
||||
|
||||
// Save file to disk
|
||||
Path absolutePath = Paths.get(STORAGE_ROOT, relativePath);
|
||||
Files.createDirectories(absolutePath.getParent());
|
||||
Files.copy(file.getInputStream(), absolutePath);
|
||||
// Save file to disk via StorageService
|
||||
storageService.store(file, Paths.get(relativePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,6 @@ public class OrderController {
|
||||
private final CustomerRepository customerRepo;
|
||||
private final com.printcalculator.service.StorageService storageService;
|
||||
|
||||
// TODO: Inject Storage Service or use a base path property
|
||||
// private static final String STORAGE_ROOT = "storage_orders";
|
||||
|
||||
public OrderController(OrderRepository orderRepo,
|
||||
OrderItemRepository orderItemRepo,
|
||||
@@ -207,7 +205,11 @@ public class OrderController {
|
||||
order.setSetupCostChf(session.getSetupCostChf());
|
||||
order.setShippingCostChf(BigDecimal.valueOf(9.00)); // Default shipping? or 0?
|
||||
order.setDiscountChf(BigDecimal.ZERO);
|
||||
// TODO: Calc implementation for shipping
|
||||
// Calculate Shipping (Basic implementation: Flat rate 9.00 if not pickup)
|
||||
// Future: Check delivery method from request if available
|
||||
if (order.getShippingCostChf() == null) {
|
||||
order.setShippingCostChf(BigDecimal.valueOf(9.00));
|
||||
}
|
||||
|
||||
BigDecimal total = subtotal.add(order.getSetupCostChf()).add(order.getShippingCostChf()).subtract(order.getDiscountChf() != null ? order.getDiscountChf() : BigDecimal.ZERO);
|
||||
order.setTotalChf(total);
|
||||
|
||||
@@ -108,7 +108,11 @@ public class QuoteController {
|
||||
try {
|
||||
file.transferTo(tempInput.toFile());
|
||||
|
||||
String slicerMachineProfile = "bambu_a1"; // TODO: Add to PrinterMachine entity
|
||||
// Use profile from machine or fallback
|
||||
String slicerMachineProfile = machine.getSlicerMachineProfile();
|
||||
if (slicerMachineProfile == null || slicerMachineProfile.isEmpty()) {
|
||||
slicerMachineProfile = "bambu_a1";
|
||||
}
|
||||
|
||||
PrintStats stats = slicerService.slice(tempInput.toFile(), slicerMachineProfile, filament, process, machineOverrides, processOverrides);
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ public class PrinterMachine {
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
@Column(name = "slicer_machine_profile")
|
||||
private String slicerMachineProfile;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -57,6 +60,14 @@ public class PrinterMachine {
|
||||
this.printerDisplayName = printerDisplayName;
|
||||
}
|
||||
|
||||
public String getSlicerMachineProfile() {
|
||||
return slicerMachineProfile;
|
||||
}
|
||||
|
||||
public void setSlicerMachineProfile(String slicerMachineProfile) {
|
||||
this.slicerMachineProfile = slicerMachineProfile;
|
||||
}
|
||||
|
||||
public Integer getBuildVolumeXMm() {
|
||||
return buildVolumeXMm;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.printcalculator.exception;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(StorageException.class)
|
||||
public ResponseEntity<?> handleStorageException(StorageException exc) {
|
||||
// Log the full exception for internal debugging
|
||||
log.error("Storage Exception occurred", exc);
|
||||
|
||||
Map<String, String> response = new HashMap<>();
|
||||
|
||||
// Check for specific virus case
|
||||
if (exc.getMessage() != null && exc.getMessage().contains("antivirus scanner")) {
|
||||
response.put("error", "Security Violation");
|
||||
// Safe message for client
|
||||
response.put("message", "File rejected by security policy.");
|
||||
response.put("code", "VIRUS_DETECTED");
|
||||
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(response);
|
||||
}
|
||||
|
||||
// Generic fallback for other storage errors to avoid leaking internal paths/details
|
||||
response.put("error", "Storage Operation Failed");
|
||||
response.put("message", "Unable to process the file upload.");
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<?> handleMaxSizeException(MaxUploadSizeExceededException exc) {
|
||||
Map<String, String> response = new HashMap<>();
|
||||
response.put("error", "File too large");
|
||||
response.put("message", "The uploaded file exceeds the maximum allowed size.");
|
||||
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(response);
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class ClamAVService {
|
||||
return true;
|
||||
} else if (result instanceof ScanResult.VirusFound) {
|
||||
Map<String, Collection<String>> viruses = ((ScanResult.VirusFound) result).getFoundViruses();
|
||||
logger.warn("Virus found: {}", viruses);
|
||||
logger.warn("VIRUS DETECTED: {}", viruses);
|
||||
return false;
|
||||
} else {
|
||||
logger.warn("Unknown scan result: {}", result);
|
||||
|
||||
@@ -89,26 +89,15 @@ public class SlicerService {
|
||||
|
||||
command.add(inputStl.getAbsolutePath());
|
||||
|
||||
logger.info("Executing Slicer: " + String.join(" ", command));
|
||||
logger.info("Slicing file: " + inputStl.getAbsolutePath() + " (Size: " + inputStl.length() + " bytes)");
|
||||
if (!inputStl.exists()) {
|
||||
throw new IOException("Input file not found: " + inputStl.getAbsolutePath());
|
||||
}
|
||||
|
||||
// 4. Run Process
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.directory(tempDir.toFile());
|
||||
// pb.inheritIO(); // Useful for debugging, but maybe capture instead?
|
||||
|
||||
Process process = pb.start();
|
||||
boolean finished = process.waitFor(5, TimeUnit.MINUTES);
|
||||
|
||||
if (!finished) {
|
||||
process.destroy();
|
||||
throw new IOException("Slicer timed out");
|
||||
}
|
||||
|
||||
if (process.exitValue() != 0) {
|
||||
// Read stderr
|
||||
String error = new String(process.getErrorStream().readAllBytes());
|
||||
throw new IOException("Slicer failed with exit code " + process.exitValue() + ": " + error);
|
||||
}
|
||||
runSlicerCommand(command, tempDir);
|
||||
|
||||
// 5. Find Output GCode
|
||||
|
||||
// 5. Find Output GCode
|
||||
// Usually [basename].gcode or plate_1.gcode
|
||||
@@ -131,9 +120,6 @@ public class SlicerService {
|
||||
// 6. Parse Results
|
||||
return gCodeParser.parse(gcodeFile);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted during slicing", e);
|
||||
} finally {
|
||||
// Cleanup temp dir
|
||||
// In production we should delete, for debugging we might want to keep?
|
||||
@@ -143,4 +129,30 @@ public class SlicerService {
|
||||
// Implementation detail: Use a utility to clean up.
|
||||
}
|
||||
}
|
||||
protected void runSlicerCommand(List<String> command, Path tempDir) throws IOException {
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.directory(tempDir.toFile());
|
||||
|
||||
try {
|
||||
Process process = pb.start();
|
||||
boolean finished = process.waitFor(5, TimeUnit.MINUTES);
|
||||
|
||||
if (!finished) {
|
||||
process.destroy();
|
||||
throw new IOException("Slicer timed out");
|
||||
}
|
||||
|
||||
if (process.exitValue() != 0) {
|
||||
// Read stderr and stdout
|
||||
String stderr = new String(process.getErrorStream().readAllBytes());
|
||||
String stdout = new String(process.getInputStream().readAllBytes()); // STDOUT
|
||||
throw new IOException("Slicer failed with exit code " + process.exitValue() +
|
||||
"\nSTDERR: " + stderr +
|
||||
"\nSTDOUT: " + stdout);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted during slicing", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.printcalculator.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.printcalculator.model.PrintStats;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
|
||||
class SlicerServiceTest {
|
||||
|
||||
@Mock
|
||||
private ProfileManager profileManager;
|
||||
|
||||
@Mock
|
||||
private GCodeParser gCodeParser;
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
private SlicerService slicerService;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
// Captured execution details
|
||||
private List<String> lastCommand;
|
||||
private Path lastTempDir;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
|
||||
// Subclass to override runSlicerCommand
|
||||
slicerService = new SlicerService("orca-slicer", profileManager, gCodeParser, mapper) {
|
||||
@Override
|
||||
protected void runSlicerCommand(List<String> command, Path tempDir) throws IOException {
|
||||
lastCommand = command;
|
||||
lastTempDir = tempDir;
|
||||
// Don't run actual process.
|
||||
// Simulate GCode output creation for the parser to find?
|
||||
// Or just let it fail at parser step since we only care about JSON generation here?
|
||||
// For a full test, we should create a dummy GCode file.
|
||||
|
||||
File stl = new File(command.get(command.size() - 1));
|
||||
String basename = stl.getName().replace(".stl", "");
|
||||
Files.createFile(tempDir.resolve(basename + ".gcode"));
|
||||
}
|
||||
};
|
||||
|
||||
// Mock Profile Responses
|
||||
ObjectNode emptyNode = mapper.createObjectNode();
|
||||
when(profileManager.getMergedProfile(anyString(), eq("machine"))).thenReturn(emptyNode.deepCopy());
|
||||
when(profileManager.getMergedProfile(anyString(), eq("filament"))).thenReturn(emptyNode.deepCopy());
|
||||
when(profileManager.getMergedProfile(anyString(), eq("process"))).thenReturn(emptyNode.deepCopy());
|
||||
|
||||
// Mock Parser
|
||||
when(gCodeParser.parse(any(File.class))).thenReturn(new PrintStats(100, "1m 40s", 10.5, 1000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSlice_WithDefaults_ShouldGenerateConfig() throws IOException {
|
||||
File dummyStl = tempDir.resolve("test.stl").toFile();
|
||||
Files.createFile(dummyStl.toPath());
|
||||
|
||||
slicerService.slice(dummyStl, "Bambu A1", "PLA", "Standard", null, null);
|
||||
|
||||
assertNotNull(lastTempDir);
|
||||
assertTrue(Files.exists(lastTempDir.resolve("process.json")));
|
||||
assertTrue(Files.exists(lastTempDir.resolve("machine.json")));
|
||||
assertTrue(Files.exists(lastTempDir.resolve("filament.json")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSlice_WithLayerHeightOverride_ShouldUpdateProcessJson() throws IOException {
|
||||
File dummyStl = tempDir.resolve("test.stl").toFile();
|
||||
Files.createFile(dummyStl.toPath());
|
||||
|
||||
Map<String, String> processOverrides = new HashMap<>();
|
||||
processOverrides.put("layer_height", "0.12");
|
||||
|
||||
slicerService.slice(dummyStl, "Bambu A1", "PLA", "Standard", null, processOverrides);
|
||||
|
||||
File processJsonFile = lastTempDir.resolve("process.json").toFile();
|
||||
ObjectNode processJson = (ObjectNode) mapper.readTree(processJsonFile);
|
||||
|
||||
assertTrue(processJson.has("layer_height"));
|
||||
assertEquals("0.12", processJson.get("layer_height").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSlice_WithInfillAndSupportOverrides_ShouldUpdateProcessJson() throws IOException {
|
||||
File dummyStl = tempDir.resolve("test.stl").toFile();
|
||||
Files.createFile(dummyStl.toPath());
|
||||
|
||||
Map<String, String> processOverrides = new HashMap<>();
|
||||
processOverrides.put("sparse_infill_density", "25%");
|
||||
processOverrides.put("enable_support", "1");
|
||||
|
||||
slicerService.slice(dummyStl, "Bambu A1", "PLA", "Standard", null, processOverrides);
|
||||
|
||||
File processJsonFile = lastTempDir.resolve("process.json").toFile();
|
||||
ObjectNode processJson = (ObjectNode) mapper.readTree(processJsonFile);
|
||||
|
||||
assertEquals("25%", processJson.get("sparse_infill_density").asText());
|
||||
assertEquals("1", processJson.get("enable_support").asText());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user