fix(back-end): file error handling
This commit is contained in:
@@ -17,11 +17,19 @@ public class ClamAVService {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(ClamAVService.class);
|
private static final Logger logger = LoggerFactory.getLogger(ClamAVService.class);
|
||||||
|
|
||||||
private final ClamavClient clamavClient;
|
private final ClamavClient clamavClient;
|
||||||
|
private final boolean enabled;
|
||||||
|
|
||||||
public ClamAVService(
|
public ClamAVService(
|
||||||
@Value("${clamav.host:localhost}") String host,
|
@Value("${clamav.host:clamav}") String host,
|
||||||
@Value("${clamav.port:3310}") int port
|
@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);
|
logger.info("Initializing ClamAV client at {}:{}", host, port);
|
||||||
ClamavClient client = null;
|
ClamavClient client = null;
|
||||||
try {
|
try {
|
||||||
@@ -33,8 +41,7 @@ public class ClamAVService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean scan(InputStream inputStream) {
|
public boolean scan(InputStream inputStream) {
|
||||||
if (clamavClient == null) {
|
if (!enabled || clamavClient == null) {
|
||||||
logger.warn("ClamAV client not initialized, skipping scan (FAIL-OPEN)");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import com.printcalculator.exception.StorageException;
|
import com.printcalculator.exception.StorageException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
@@ -39,20 +41,24 @@ public class FileSystemStorageService implements StorageService {
|
|||||||
public void store(MultipartFile file, Path destinationRelativePath) throws IOException {
|
public void store(MultipartFile file, Path destinationRelativePath) throws IOException {
|
||||||
Path destinationFile = this.rootLocation.resolve(destinationRelativePath).normalize().toAbsolutePath();
|
Path destinationFile = this.rootLocation.resolve(destinationRelativePath).normalize().toAbsolutePath();
|
||||||
if (!destinationFile.getParent().startsWith(this.rootLocation.toAbsolutePath())) {
|
if (!destinationFile.getParent().startsWith(this.rootLocation.toAbsolutePath())) {
|
||||||
// Security check
|
|
||||||
throw new StorageException("Cannot store file outside current directory.");
|
throw new StorageException("Cannot store file outside current directory.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan stream (Read 1)
|
// 1. Salva prima il file su disco per evitare problemi di stream con file grandi
|
||||||
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)
|
|
||||||
Files.createDirectories(destinationFile.getParent());
|
Files.createDirectories(destinationFile.getParent());
|
||||||
file.transferTo(destinationFile.toFile());
|
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
|
@Override
|
||||||
@@ -62,11 +68,6 @@ public class FileSystemStorageService implements StorageService {
|
|||||||
throw new StorageException("Cannot store file outside current directory.");
|
throw new StorageException("Cannot store file outside current directory.");
|
||||||
}
|
}
|
||||||
Files.createDirectories(destinationFile.getParent());
|
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);
|
Files.copy(source, destinationFile, StandardCopyOption.REPLACE_EXISTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ public class SlicerService {
|
|||||||
public PrintStats slice(File inputStl, String machineName, String filamentName, String processName,
|
public PrintStats slice(File inputStl, String machineName, String filamentName, String processName,
|
||||||
Map<String, String> machineOverrides, Map<String, String> processOverrides) throws IOException {
|
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 machineProfile = profileManager.getMergedProfile(machineName, "machine");
|
||||||
ObjectNode filamentProfile = profileManager.getMergedProfile(filamentName, "filament");
|
ObjectNode filamentProfile = profileManager.getMergedProfile(filamentName, "filament");
|
||||||
ObjectNode processProfile = profileManager.getMergedProfile(processName, "process");
|
ObjectNode processProfile = profileManager.getMergedProfile(processName, "process");
|
||||||
@@ -50,18 +53,14 @@ public class SlicerService {
|
|||||||
if (machineOverrides != null) machineOverrides.forEach(machineProfile::put);
|
if (machineOverrides != null) machineOverrides.forEach(machineProfile::put);
|
||||||
if (processOverrides != null) processOverrides.forEach(processProfile::put);
|
if (processOverrides != null) processOverrides.forEach(processProfile::put);
|
||||||
|
|
||||||
// Pulizia profonda per evitare errori di inizializzazione grafica/piatto
|
// Pulizia radicale per rendere la macchina "anonima" ed evitare crash geometrici su zone di esclusione
|
||||||
machineProfile.remove("bed_exclude_area");
|
makeMachineGeneric(machineProfile);
|
||||||
machineProfile.remove("head_wrap_detect_zone");
|
|
||||||
machineProfile.remove("bed_custom_model");
|
|
||||||
machineProfile.remove("bed_custom_texture");
|
|
||||||
|
|
||||||
Path baseTempPath = Paths.get("/app/temp");
|
Path baseTempPath = Paths.get("/app/temp");
|
||||||
if (!Files.exists(baseTempPath)) Files.createDirectories(baseTempPath);
|
if (!Files.exists(baseTempPath)) Files.createDirectories(baseTempPath);
|
||||||
Path tempDir = Files.createTempDirectory(baseTempPath, "job_");
|
Path tempDir = Files.createTempDirectory(baseTempPath, "job_");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Usiamo un nome file senza spazi per massima compatibilità CLI
|
|
||||||
File localStl = tempDir.resolve("input.stl").toFile();
|
File localStl = tempDir.resolve("input.stl").toFile();
|
||||||
Files.copy(inputStl.toPath(), localStl.toPath());
|
Files.copy(inputStl.toPath(), localStl.toPath());
|
||||||
|
|
||||||
@@ -76,7 +75,7 @@ public class SlicerService {
|
|||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add(slicerPath);
|
command.add(slicerPath);
|
||||||
|
|
||||||
// Parametri di caricamento
|
// Ordine ottimizzato per OrcaSlicer 1.9+
|
||||||
command.add("--load-settings");
|
command.add("--load-settings");
|
||||||
command.add(mFile.getAbsolutePath());
|
command.add(mFile.getAbsolutePath());
|
||||||
command.add("--load-settings");
|
command.add("--load-settings");
|
||||||
@@ -84,26 +83,23 @@ public class SlicerService {
|
|||||||
command.add("--load-filaments");
|
command.add("--load-filaments");
|
||||||
command.add(fFile.getAbsolutePath());
|
command.add(fFile.getAbsolutePath());
|
||||||
|
|
||||||
// Output
|
|
||||||
command.add("--outputdir");
|
command.add("--outputdir");
|
||||||
command.add(tempDir.toAbsolutePath().toString());
|
command.add(tempDir.toAbsolutePath().toString());
|
||||||
|
|
||||||
// Slicing & Auto-center (fondamentale per evitare "Nothing to be sliced")
|
|
||||||
command.add("--arrange");
|
command.add("--arrange");
|
||||||
command.add("1");
|
command.add("1");
|
||||||
command.add("--ensure-on-bed");
|
command.add("--ensure-on-bed");
|
||||||
|
|
||||||
command.add("--slice");
|
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());
|
command.add(localStl.getAbsolutePath());
|
||||||
|
|
||||||
logger.info("Executing Slicer on file: " + localStl.getAbsolutePath() + " (Size: " + localStl.length() + " bytes)");
|
logger.info("Executing Slicer on file: " + localStl.getAbsolutePath() + " (Size: " + localStl.length() + " bytes)");
|
||||||
|
|
||||||
runSlicerCommand(command, tempDir);
|
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)) {
|
try (Stream<Path> s = Files.list(tempDir)) {
|
||||||
Optional<Path> found = s.filter(p -> p.toString().endsWith(".gcode")).findFirst();
|
Optional<Path> found = s.filter(p -> p.toString().endsWith(".gcode")).findFirst();
|
||||||
if (found.isPresent()) return gCodeParser.parse(found.get().toFile());
|
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 {
|
protected void runSlicerCommand(List<String> command, Path tempDir) throws IOException, InterruptedException {
|
||||||
ProcessBuilder pb = new ProcessBuilder(command);
|
ProcessBuilder pb = new ProcessBuilder(command);
|
||||||
pb.directory(tempDir.toFile());
|
pb.directory(tempDir.toFile());
|
||||||
|
|
||||||
// Variabili d'ambiente minimali ma necessarie
|
|
||||||
Map<String, String> env = pb.environment();
|
Map<String, String> env = pb.environment();
|
||||||
env.put("HOME", "/tmp");
|
env.put("HOME", "/tmp");
|
||||||
env.put("QT_QPA_PLATFORM", "offscreen");
|
env.put("QT_QPA_PLATFORM", "offscreen");
|
||||||
|
|||||||
@@ -18,3 +18,8 @@ profiles.root=${PROFILES_DIR:profiles}
|
|||||||
# File Upload Limits
|
# File Upload Limits
|
||||||
spring.servlet.multipart.max-file-size=200MB
|
spring.servlet.multipart.max-file-size=200MB
|
||||||
spring.servlet.multipart.max-request-size=200MB
|
spring.servlet.multipart.max-request-size=200MB
|
||||||
|
|
||||||
|
# ClamAV Configuration
|
||||||
|
clamav.host=${CLAMAV_HOST:clamav}
|
||||||
|
clamav.port=${CLAMAV_PORT:3310}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user