fix(back-end): file error handling
Some checks failed
Build, Test and Deploy / test-backend (push) Failing after 27s
Build, Test and Deploy / build-and-push (push) Has been skipped
Build, Test and Deploy / deploy (push) Has been skipped

This commit is contained in:
2026-02-16 15:23:02 +01:00
parent a96c28fb39
commit 91af8f4f9c
4 changed files with 70 additions and 32 deletions

View File

@@ -17,11 +17,19 @@ public class ClamAVService {
private static final Logger logger = LoggerFactory.getLogger(ClamAVService.class);
private final ClamavClient clamavClient;
private final boolean enabled;
public ClamAVService(
@Value("${clamav.host:localhost}") String host,
@Value("${clamav.port:3310}") int port
@Value("${clamav.host:clamav}") String host,
@Value("${clamav.port:3310}") int port,
@Value("${clamav.enabled:false}") boolean enabled
) {
this.enabled = enabled;
if (!enabled) {
logger.info("ClamAV is DISABLED");
this.clamavClient = null;
return;
}
logger.info("Initializing ClamAV client at {}:{}", host, port);
ClamavClient client = null;
try {
@@ -33,8 +41,7 @@ public class ClamAVService {
}
public boolean scan(InputStream inputStream) {
if (clamavClient == null) {
logger.warn("ClamAV client not initialized, skipping scan (FAIL-OPEN)");
if (!enabled || clamavClient == null) {
return true;
}
try {

View File

@@ -7,6 +7,8 @@ import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.printcalculator.exception.StorageException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
@@ -39,20 +41,24 @@ public class FileSystemStorageService implements StorageService {
public void store(MultipartFile file, Path destinationRelativePath) throws IOException {
Path destinationFile = this.rootLocation.resolve(destinationRelativePath).normalize().toAbsolutePath();
if (!destinationFile.getParent().startsWith(this.rootLocation.toAbsolutePath())) {
// Security check
throw new StorageException("Cannot store file outside current directory.");
}
// Scan stream (Read 1)
try (InputStream inputStream = file.getInputStream()) {
if (!clamAVService.scan(inputStream)) {
throw new StorageException("File rejected by antivirus scanner.");
}
}
// Save to disk (Using transferTo which is safer than opening another stream)
// 1. Salva prima il file su disco per evitare problemi di stream con file grandi
Files.createDirectories(destinationFile.getParent());
file.transferTo(destinationFile.toFile());
// 2. Scansiona il file appena salvato aprendo un nuovo stream
try (InputStream inputStream = new FileInputStream(destinationFile.toFile())) {
if (!clamAVService.scan(inputStream)) {
// Se infetto, cancella il file e solleva eccezione
Files.deleteIfExists(destinationFile);
throw new StorageException("File rejected by antivirus scanner.");
}
} catch (Exception e) {
if (e instanceof StorageException) throw e;
// Se l'antivirus fallisce per motivi tecnici, lasciamo il file (fail-open come concordato)
}
}
@Override
@@ -62,11 +68,6 @@ public class FileSystemStorageService implements StorageService {
throw new StorageException("Cannot store file outside current directory.");
}
Files.createDirectories(destinationFile.getParent());
// We assume source is already safe/scanned if it is internal?
// Or should we scan it too?
// If it comes from QuoteSession (which was scanned on upload), it is safe.
// If we want to be paranoid, we can scan again, but maybe overkill.
// Let's assume it is safe for internal copies.
Files.copy(source, destinationFile, StandardCopyOption.REPLACE_EXISTING);
}

View File

@@ -43,6 +43,9 @@ public class SlicerService {
public PrintStats slice(File inputStl, String machineName, String filamentName, String processName,
Map<String, String> machineOverrides, Map<String, String> processOverrides) throws IOException {
// Log version once for diagnostics
try { runVersionCheck(); } catch (Exception e) {}
ObjectNode machineProfile = profileManager.getMergedProfile(machineName, "machine");
ObjectNode filamentProfile = profileManager.getMergedProfile(filamentName, "filament");
ObjectNode processProfile = profileManager.getMergedProfile(processName, "process");
@@ -50,18 +53,14 @@ public class SlicerService {
if (machineOverrides != null) machineOverrides.forEach(machineProfile::put);
if (processOverrides != null) processOverrides.forEach(processProfile::put);
// Pulizia profonda per evitare errori di inizializzazione grafica/piatto
machineProfile.remove("bed_exclude_area");
machineProfile.remove("head_wrap_detect_zone");
machineProfile.remove("bed_custom_model");
machineProfile.remove("bed_custom_texture");
// Pulizia radicale per rendere la macchina "anonima" ed evitare crash geometrici su zone di esclusione
makeMachineGeneric(machineProfile);
Path baseTempPath = Paths.get("/app/temp");
if (!Files.exists(baseTempPath)) Files.createDirectories(baseTempPath);
Path tempDir = Files.createTempDirectory(baseTempPath, "job_");
try {
// Usiamo un nome file senza spazi per massima compatibilità CLI
File localStl = tempDir.resolve("input.stl").toFile();
Files.copy(inputStl.toPath(), localStl.toPath());
@@ -76,7 +75,7 @@ public class SlicerService {
List<String> command = new ArrayList<>();
command.add(slicerPath);
// Parametri di caricamento
// Ordine ottimizzato per OrcaSlicer 1.9+
command.add("--load-settings");
command.add(mFile.getAbsolutePath());
command.add("--load-settings");
@@ -84,26 +83,23 @@ public class SlicerService {
command.add("--load-filaments");
command.add(fFile.getAbsolutePath());
// Output
command.add("--outputdir");
command.add(tempDir.toAbsolutePath().toString());
// Slicing & Auto-center (fondamentale per evitare "Nothing to be sliced")
command.add("--arrange");
command.add("1");
command.add("--ensure-on-bed");
command.add("--slice");
command.add("0"); // 0 solitamente significa "tutti i piatti con oggetti"
command.add("1"); // Plate 1
// File da processare (sempre per ultimo)
command.add(localStl.getAbsolutePath());
logger.info("Executing Slicer on file: " + localStl.getAbsolutePath() + " (Size: " + localStl.length() + " bytes)");
runSlicerCommand(command, tempDir);
// Cerca il file G-code prodotto (input.gcode o simili)
// Cerca il file G-code prodotto
try (Stream<Path> s = Files.list(tempDir)) {
Optional<Path> found = s.filter(p -> p.toString().endsWith(".gcode")).findFirst();
if (found.isPresent()) return gCodeParser.parse(found.get().toFile());
@@ -116,11 +112,40 @@ public class SlicerService {
}
}
private void makeMachineGeneric(ObjectNode profile) {
// Rimuove l'identità della stampante per forzare lo slicer a usare solo i dati geometrici che forniamo
profile.remove("inherits");
profile.remove("printer_model");
profile.remove("printer_variant");
profile.remove("setting_id");
profile.remove("printer_settings_id");
// Rimuove zone di esclusione e modelli complessi che richiedono calcoli grafici pesanti
profile.remove("bed_exclude_area");
profile.remove("head_wrap_detect_zone");
profile.remove("bed_custom_model");
profile.remove("bed_custom_texture");
profile.remove("thumbnail");
profile.remove("thumbnails");
// Forza un'area di stampa standard 256x256 (Bambu A1)
try {
profile.set("printable_area", mapper.readTree("[\"0x0\",\"256x0\",\"256x256\",\"0x256\"]"));
profile.put("printable_height", "256");
} catch (Exception ignored) {}
}
private void runVersionCheck() throws IOException, InterruptedException {
Process p = new ProcessBuilder(slicerPath, "--version").start();
p.waitFor();
String ver = new String(p.getInputStream().readAllBytes()).trim();
logger.info("OrcaSlicer Version on server: " + ver);
}
protected void runSlicerCommand(List<String> command, Path tempDir) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
pb.directory(tempDir.toFile());
// Variabili d'ambiente minimali ma necessarie
Map<String, String> env = pb.environment();
env.put("HOME", "/tmp");
env.put("QT_QPA_PLATFORM", "offscreen");

View File

@@ -18,3 +18,8 @@ profiles.root=${PROFILES_DIR:profiles}
# File Upload Limits
spring.servlet.multipart.max-file-size=200MB
spring.servlet.multipart.max-request-size=200MB
# ClamAV Configuration
clamav.host=${CLAMAV_HOST:clamav}
clamav.port=${CLAMAV_PORT:3310}