Sincronizzare due cartelle: la via più semplice senza software pesanti
Ti è mai capitato di voler semplicemente copiare i file nuovi o modificati da una cartella all’altra? Magari devi fare un backup al volo su un hard disk esterno, aggiornare una directory di lavoro su una chiavetta, o allineare due archivi.
L’istinto, al giorno d’oggi, è quello di aprire il browser e cercare un programma dedicato. Il risultato? Ti ritrovi a scaricare “programmoni” da centinaia di megabyte, client cloud che ti chiedono di creare un account, o shareware che ti assillano con pop-up per passare alla versione “Pro”. Tutto questo per fare un’operazione che i computer sanno eseguire in modo nativo dagli anni ’70: confrontare due liste di file e copiare le differenze. È un inutile spreco di tempo e risorse.
Non abbiamo bisogno di un’astronave per attraversare la strada. In un ambiente informatico sempre più caotico, la soluzione migliore è quasi sempre quella più essenziale. Su Windows, ad esempio, esiste già uno strumento potentissimo sotto il cofano: si chiama robocopy. È un mostro di affidabilità, ma vive nel terminale. E siamo onesti: dover aprire il prompt e ricordare a memoria decine di parametri (come /MIR, /R:0, /W:5) è una tortura e un rischio per chi non è concentrato al cento per cento.
La soluzione più intelligente? Creare un ponte. Un’interfaccia grafica ridotta all’osso che prenda la potenza grezza di robocopy e la renda usabile in due clic, senza dover installare nulla di pesante.
Il nostro strumento: JRobo
Abbiamo scritto un piccolo programma in Java. Niente installazioni chilometriche, niente chiavi di registro sporcate. JRobo è un’interfaccia pulita che fa esattamente quello che promette, senza fronzoli. Ti permette di selezionare una cartella di origine, una di destinazione e ti offre due opzioni fondamentali:
- Simulare le differenze: Una funzione salvavita. Il programma analizza le cartelle e ti dice esattamente cosa verrebbe copiato e quanti byte verrebbero spostati, senza toccare un singolo file. Ottimo per evitare disastri.
- Avviare il mirroring reale: Sincronizza le cartelle in modo speculare, affidando il lavoro sporco al sistema operativo.
Ecco il codice completo, da salvare in un file chiamato JRobo.java:
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
public class JRobo extends JFrame {
// --- CONFIGURAZIONE VISIVA ---
private static final Color BG_COLOR = new Color(30, 30, 30);
private static final Color FG_COLOR = new Color(0, 255, 0);
private static final Color INFO_COLOR = new Color(0, 150, 255);
private static final Color ERR_COLOR = new Color(255, 50, 50);
private static final Color ACCENT_COLOR = new Color(50, 50, 50);
private static final Font TERMINAL_FONT = new Font("Consolas", Font.PLAIN, 12);
private static final String CONFIG_FILE = "jrobo_config.properties";
private JTextField txtSource = new JTextField();
private JTextField txtDest = new JTextField();
private JTextArea txtLog = new JTextArea();
private JButton btnAction = new JButton(">>> AVVIA MIRRORING <<<");
private JButton btnSimulate = new JButton("🔍 CALCOLA DIFFERENZE (SIMULA)");
private JProgressBar progressBar = new JProgressBar();
private JLabel lblStatus = new JLabel("In attesa...");
private volatile Process currentProcess = null;
private AtomicBoolean isRunning = new AtomicBoolean(false);
public JRobo() {
super("JRobo :: Mirroring System v6.1 (Conflict Solver)");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(900, 700);
setLocationRelativeTo(null);
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBackground(BG_COLOR);
mainPanel.setBorder(new EmptyBorder(15, 15, 15, 15));
// INPUT
JPanel inputPanel = new JPanel(new GridLayout(2, 1, 10, 10));
inputPanel.setBackground(BG_COLOR);
inputPanel.add(createPathPanel("SORGENTE [DIR]:", txtSource));
inputPanel.add(createPathPanel("DESTINAZIONE [DIR]:", txtDest));
mainPanel.add(inputPanel, BorderLayout.NORTH);
// LOG
txtLog.setBackground(new Color(10, 10, 10));
txtLog.setForeground(FG_COLOR);
txtLog.setFont(TERMINAL_FONT);
txtLog.setEditable(false);
txtLog.setBorder(new EmptyBorder(5, 5, 5, 5));
JScrollPane scrollPane = new JScrollPane(txtLog);
scrollPane.setBorder(new LineBorder(FG_COLOR));
scrollPane.getViewport().setBackground(new Color(10, 10, 10));
mainPanel.add(scrollPane, BorderLayout.CENTER);
// COMANDI
JPanel bottomContainer = new JPanel(new BorderLayout(5, 5));
bottomContainer.setBackground(BG_COLOR);
progressBar.setStringPainted(true);
progressBar.setForeground(Color.BLACK);
progressBar.setBackground(ACCENT_COLOR);
progressBar.setBorderPainted(false);
progressBar.setString("PRONTO");
btnAction.setFont(new Font("SansSerif", Font.BOLD, 14));
btnAction.setFocusPainted(false);
btnAction.setBorder(BorderFactory.createCompoundBorder(new LineBorder(Color.WHITE, 1), new EmptyBorder(10, 0, 10, 0)));
btnAction.setCursor(new Cursor(Cursor.HAND_CURSOR));
btnSimulate.setBackground(INFO_COLOR);
btnSimulate.setForeground(Color.WHITE);
btnSimulate.setFont(new Font("SansSerif", Font.BOLD, 12));
btnSimulate.setFocusPainted(false);
btnSimulate.setBorder(BorderFactory.createCompoundBorder(new LineBorder(Color.WHITE, 1), new EmptyBorder(8, 0, 8, 0)));
btnSimulate.setCursor(new Cursor(Cursor.HAND_CURSOR));
btnSimulate.setOpaque(true);
setButtonState(false);
btnAction.addActionListener(e -> {
if (isRunning.get()) {
forceKill(); // STOP MANUALE
} else {
saveConfig();
new Thread(() -> runRobocopy(false)).start();
}
});
btnSimulate.addActionListener(e -> {
if (!isRunning.get()) {
saveConfig();
new Thread(() -> runRobocopy(true)).start();
}
});
JPanel buttonPanel = new JPanel(new GridLayout(2, 1, 5, 5));
buttonPanel.setBackground(BG_COLOR);
buttonPanel.add(btnSimulate);
buttonPanel.add(btnAction);
lblStatus.setForeground(Color.LIGHT_GRAY);
lblStatus.setHorizontalAlignment(SwingConstants.CENTER);
lblStatus.setFont(new Font("SansSerif", Font.ITALIC, 12));
JPanel commandsPanel = new JPanel(new BorderLayout(0, 10));
commandsPanel.setBackground(BG_COLOR);
commandsPanel.add(progressBar, BorderLayout.NORTH);
commandsPanel.add(buttonPanel, BorderLayout.CENTER);
commandsPanel.add(lblStatus, BorderLayout.SOUTH);
bottomContainer.add(commandsPanel, BorderLayout.SOUTH);
mainPanel.add(bottomContainer, BorderLayout.SOUTH);
add(mainPanel);
loadConfig();
setVisible(true);
}
private void setButtonState(boolean running) {
if (running) {
btnAction.setText(">>> FERMA TUTTO (KILL PROCESS) <<<");
btnAction.setBackground(ERR_COLOR);
btnAction.setForeground(Color.WHITE);
btnSimulate.setEnabled(false);
} else {
btnAction.setText(">>> AVVIA MIRRORING REALE <<<");
btnAction.setBackground(FG_COLOR);
btnAction.setForeground(Color.BLACK);
btnSimulate.setEnabled(true);
}
btnAction.setOpaque(true);
}
// Uccide il processo e aspetta che Windows rilasci i file
private void forceKill() {
if (currentProcess != null && currentProcess.isAlive()) {
appendLog("\n>>> PULIZIA PROCESSI IN CORSO...");
currentProcess.destroyForcibly();
try { Thread.sleep(1000); } catch (InterruptedException e) {} // Pausa di sicurezza
appendLog(">>> PROCESSO TERMINATO. FILE RILASCIATI.");
}
}
private void runRobocopy(boolean isSimulation) {
// 1. PULIZIA PREVENTIVA: Se c'era qualcosa di appeso, lo uccidiamo prima di partire
forceKill();
String src = txtSource.getText();
String dst = txtDest.getText();
if (src.isEmpty() || dst.isEmpty()) {
appendLog("ERRORE: Seleziona le cartelle!");
return;
}
isRunning.set(true);
SwingUtilities.invokeLater(() -> {
setButtonState(true);
progressBar.setIndeterminate(true);
progressBar.setString(isSimulation ? "CALCOLO IN CORSO..." : "COPIA IN CORSO...");
txtLog.setText("");
});
try {
String mode = isSimulation ? "SIMULAZIONE (/L)" : "MIRRORING REALE (/MIR)";
appendLog(">>> AVVIO " + mode + "...");
appendLog("DA: " + src);
appendLog("A: " + dst);
appendLog("--------------------------------------------------");
ProcessBuilder pb;
if (isSimulation) {
// SIMULAZIONE: /R:0 /W:0 -> Non aspetta mai i file bloccati, va dritto come un treno
pb = new ProcessBuilder("robocopy", src, dst, "/MIR", "/L", "/BYTES", "/R:0", "/W:0", "/NP", "/NDL");
} else {
// REALE: /W:5 -> Aspetta 5 secondi se un file è bloccato (dà tempo all'antivirus di finire)
pb = new ProcessBuilder("robocopy", src, dst, "/MIR", "/NP", "/R:3", "/W:5", "/NFL", "/NDL");
}
pb.redirectErrorStream(true);
currentProcess = pb.start();
Charset dosCharset = Charset.forName("IBM850");
BufferedReader reader = new BufferedReader(new InputStreamReader(currentProcess.getInputStream(), dosCharset));
String line;
String lastBytesLine = "";
try {
while ((line = reader.readLine()) != null) {
if (!line.trim().isEmpty() && !line.contains("%")) {
if (isSimulation) {
appendLog(line);
if (line.trim().startsWith("Bytes") || line.trim().startsWith("Byte")) {
lastBytesLine = line;
}
} else {
appendLog(line);
}
}
}
} catch (IOException ioEx) { }
int exitCode = currentProcess.waitFor();
appendLog("--------------------------------------------------");
if (isSimulation && !lastBytesLine.isEmpty()) {
parseAndShowStats(lastBytesLine);
}
if (currentProcess.exitValue() != 0 && exitCode > 7) {
appendLog(">>> ATTENZIONE: Possibili errori o interruzione (Codice: " + exitCode + ")");
} else {
appendLog(">>> OPERAZIONE COMPLETATA.");
}
} catch (InterruptedException ie) {
appendLog(">>> INTERRUZIONE UTENTE.");
} catch (Exception ex) {
appendLog("ERRORE CRITICO: " + ex.getMessage());
} finally {
currentProcess = null;
isRunning.set(false);
SwingUtilities.invokeLater(() -> {
setButtonState(false);
progressBar.setIndeterminate(false);
progressBar.setString("PRONTO");
});
}
}
private void parseAndShowStats(String byteLine) {
try {
String clean = byteLine.replace("Bytes :", "").replace("Byte :", "").trim();
String[] parts = clean.split("\\s+");
if (parts.length >= 2) {
long totalBytes = Long.parseLong(parts[0]);
long bytesToCopy = Long.parseLong(parts[1]);
String msgTotal = humanReadableByteCount(totalBytes);
String msgToCopy = humanReadableByteCount(bytesToCopy);
SwingUtilities.invokeLater(() -> {
lblStatus.setText("TOTALE: " + msgTotal + " | DA TRASFERIRE: " + msgToCopy);
JOptionPane.showMessageDialog(this,
"SIMULAZIONE COMPLETATA\n\n" +
"Dati che verranno copiati: " + msgToCopy + "\n" +
"(Su un totale sorgente di " + msgTotal + ")",
"Report", JOptionPane.INFORMATION_MESSAGE);
});
}
} catch (Exception e) {}
}
private String humanReadableByteCount(long bytes) {
if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024));
String pre = "KMGTPE".charAt(exp-1) + "";
return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre);
}
private JPanel createPathPanel(String labelText, JTextField textField) {
JPanel p = new JPanel(new BorderLayout(5, 5));
p.setBackground(BG_COLOR);
JLabel lbl = new JLabel(labelText);
lbl.setForeground(Color.WHITE);
lbl.setFont(new Font("SansSerif", Font.BOLD, 12));
lbl.setPreferredSize(new Dimension(150, 30));
textField.setBackground(ACCENT_COLOR);
textField.setForeground(Color.WHITE);
textField.setCaretColor(Color.WHITE);
textField.setBorder(BorderFactory.createCompoundBorder(new LineBorder(Color.GRAY), new EmptyBorder(5, 5, 5, 5)));
JButton btnBrowse = new JButton("📂");
btnBrowse.setBackground(new Color(60, 60, 60));
btnBrowse.setForeground(Color.WHITE);
btnBrowse.setFocusPainted(false);
btnBrowse.setBorder(new LineBorder(Color.GRAY));
btnBrowse.setPreferredSize(new Dimension(50, 30));
btnBrowse.setOpaque(true);
btnBrowse.addActionListener(e -> textField.setText(pickFolder()));
p.add(lbl, BorderLayout.WEST);
p.add(textField, BorderLayout.CENTER);
p.add(btnBrowse, BorderLayout.EAST);
return p;
}
private String pickFolder() {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
return (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) ? chooser.getSelectedFile().getAbsolutePath() : "";
}
private void appendLog(String text) {
SwingUtilities.invokeLater(() -> {
txtLog.append(text + "\n");
txtLog.setCaretPosition(txtLog.getDocument().getLength());
});
}
private void loadConfig() {
File f = new File(CONFIG_FILE);
if (f.exists()) {
try (FileInputStream in = new FileInputStream(f)) {
Properties props = new Properties();
props.load(in);
txtSource.setText(props.getProperty("source", ""));
txtDest.setText(props.getProperty("dest", ""));
appendLog(">>> Configurazione caricata.");
} catch (IOException e) {}
}
}
private void saveConfig() {
try (FileOutputStream out = new FileOutputStream(CONFIG_FILE)) {
Properties props = new Properties();
props.setProperty("source", txtSource.getText());
props.setProperty("dest", txtDest.getText());
props.store(out, "JRobo Config");
} catch (IOException e) {
appendLog(">>> Errore salvataggio config: " + e.getMessage());
}
}
public static void main(String[] args) {
// Disabilitato il tema di sistema per forzare il corretto rendering dei colori (Tema Metal)
// try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception ignored) {}
SwingUtilities.invokeLater(JRobo::new);
}
}
L’automazione: zero sbattimenti per l’avvio
Ovviamente, un file .java va compilato prima di poter essere utilizzato. Ma chi ha voglia di aprire il prompt dei comandi ogni volta per dare le istruzioni di compilazione al sistema? L’obiettivo qui è la comodità.
Per questo, abbiamo preparato uno script .cmd che fa tutto in autonomia. Lo avvii con un doppio clic, lui controlla se il programma è già stato compilato: se non lo è (ad esempio al primissimo avvio o se hai fatto modifiche al codice), lo compila e poi lo fa partire. E se qualcosa dovesse andare storto durante l’esecuzione, lo script si mette in pausa per permetterti di leggere l’errore, anziché chiuderti sgarbatamente la finestra in faccia.
Crea un file chiamato start.cmd (o qualsiasi nome preferisci) nella stessa cartella dove hai salvato JRobo.java, e incolla questo:
@echo off
:: 1. COMANDO MAGICO: Costringe il prompt a spostarsi nella cartella dove risiede questo file .bat
cd /d "%~dp0"
echo Sto cercando di avviare JRobo...
echo Cartella di lavoro: %CD%
:: 2. Controllo se esiste il file compilato
if not exist "JRobo.class" (
echo.
echo ATTENZIONE: Non trovo JRobo.class!
echo Provo a compilarlo al volo...
javac JRobo.java
if errorlevel 1 (
echo ERRORE DI COMPILAZIONE. Controlla il codice o il JDK.
pause
exit
)
echo Compilazione riuscita.
)
:: 3. Avvio (Uso java normale e non javaw così vediamo l'output se crasha all'avvio)
:: -cp . significa "cerca le classi in QUESTA cartella"
java -cp . JRobo
:: 4. Se il programma Java esce con un errore, metto in pausa
if %errorlevel% neq 0 (
echo.
echo ==============================================
echo IL PROGRAMMA E' CRASHATO O USCITO CON ERRORE.
echo LEGGI SOPRA COSA E' SUCCESSO.
echo ==============================================
pause
)
Finito. La prossima volta che dovrai allineare due directory, ti basterà un doppio clic sul tuo .cmd. Nessuna barra di caricamento fasulla, nessuna distrazione, nessun abbonamento cloud. Solo il tuo sistema operativo che obbedisce ai tuoi comandi in modo rapido e silenzioso.
Una volta compilato la prima volta, il file
.classrimarrà lì e i successivi avvii saranno istantanei. Se modifichi il codice Java, cancella il file.classe lo script lo ricompilerà automaticamente al prossimo avvio.
Un piccolo dettaglio: la memoria (jrobo_config.properties)
C’è un’ultima cosa che rende questo script davvero comodo: non soffre di amnesia. Se fai un backup periodico sempre delle stesse cartelle, doverle riselezionare ogni volta diventerebbe in fretta una tortura.
Per evitarlo, ogni volta che avvii un’operazione JRobo crea silenziosamente un minuscolo file di testo nella sua stessa cartella, chiamato jrobo_config.properties. È la sua memoria a breve termine. Lì dentro si appunta l’ultimo percorso di origine e di destinazione che hai usato. La volta successiva che avvierai il programma, troverai i campi di testo già compilati e pronti all’uso. Se non ti serve, puoi cancellare il file quando vuoi; se ti serve, ti risparmia quei due clic in più che fanno la differenza.