diff --git a/README.md b/README.md new file mode 100644 index 0000000..4625bdd --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +The purpose of this small application is to synchronize two initially similar directories, but one of which has renamed and moved files, to apply the same modifications to the second. + +The typical use case is that of a backup on 'dir1' of 'dir2'; after classifying files in dir2 (renaming and moving files in sub-directories), if we make a new backup, we are offered copy+deletes on dir1 which is inefficient. + +The application detects the actions performed on dir2, relying on the size and the date of update (assumed unchanged by the classification), and proposes to perform the same renaming/moving operations on the target directory dir1. + +![sscreenshot](./screenshot.png "") + +Operating mode : choose directories 1 (target that will be modified) and 2 (model), press 'search'; select the items to update then press 'execute'. + +A setting allows to log the actions in a file, to round the times to the second and to choose the language. diff --git a/nbactions.xml b/nbactions.xml new file mode 100644 index 0000000..d3e3d0e --- /dev/null +++ b/nbactions.xml @@ -0,0 +1,20 @@ + + + + run + + jar + + + process-classes + org.codehaus.mojo:exec-maven-plugin:3.0.0:exec + + + + ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} + + uriot.renmov.MainFrame + java + + + diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000..f71adc0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + uriot + renmov + 1.0 + + + UTF-8 + 17 + 17 + + + + + commons-io + commons-io + 2.11.0 + + + org.apache.commons + commons-collections4 + 4.4 + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + ${project.version} + + + uriot.renmov.MainFrame + + + + + + com.github.wvengen + proguard-maven-plugin + 2.5.3 + + + package + proguard + + + + ${project.build.finalName}.jar + + ${java.home}/jmods/java.base.jmod + ${java.home}/jmods/java.logging.jmod + ${java.home}/jmods/java.desktop.jmod + + true + ${project.build.finalName}-small.jar + ${project.build.directory} + ${basedir}/proguard.conf + + + + + + \ No newline at end of file diff --git a/proguard.conf b/proguard.conf new file mode 100644 index 0000000..2a8a85b --- /dev/null +++ b/proguard.conf @@ -0,0 +1,32 @@ +-dontnote +-dontwarn + +-forceprocessing +-verbose + +# {{{ all three of these make the "don't touch anything" +# these are listed in the order they happen in its flow +# -dontshrink +# -dontoptimize +-dontobfuscate +# }}} + +-adaptresourcefilenames **.properties,**.xml,META-INF/MANIFEST.MF,META-INF/spring.* +-adaptresourcefilecontents **.properties,**.xml,META-INF/MANIFEST.MF,META-INF/spring.* + +-keep public class uriot.renmov.MainFrame { + public static void main(java.lang.String[]); +} + +-keep,allowshrinking class * extends java.io.Serializable +-keepclassmembers class * extends java.io.Serializable { + *; +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod + diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..46b7310 Binary files /dev/null and b/screenshot.png differ diff --git a/src/main/java/uriot/renmov/Controller.java b/src/main/java/uriot/renmov/Controller.java new file mode 100644 index 0000000..371416d --- /dev/null +++ b/src/main/java/uriot/renmov/Controller.java @@ -0,0 +1,129 @@ +package uriot.renmov; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.commons.io.FileUtils; + +public class Controller { + + public Logger log; + public Settings settings; + public ResourceBundle bundle; + + void search(ArrayList items, String dir1, String dir2, int[] res) { + items.clear(); + + if (!new File(dir1).exists() || !new File(dir2).exists()) { + res[0] = 1; + return; + } + + var map = new ArrayListValuedHashMap(); + try (var walk = Files.walk(Path.of(new URI("file:///" + dir2)))) { + walk.filter(Files::isRegularFile).map(path -> { + var file = path.toFile(); + Criteria key = null; + try { + var date = ((FileTime) Files.getAttribute(path, "creationTime")).toInstant(); + key = new Criteria(date, file.length(), settings.roundToSecond); + } + catch (IOException ex) { + log.log(Level.SEVERE, null, ex); + res[0] = 2; + } + var item = new Item(key); + item.path2 = file.getPath(); + return item; + }) + .forEach(item -> { + map.put(item.criteria, item); + }); + } + catch (URISyntaxException | IOException ex) { + log.log(Level.SEVERE, null, ex); + res[0] = 2; + } + + try (var walk = Files.walk(Path.of(new URI("file:///" + dir1)))) { + walk.filter(Files::isRegularFile) + .mapMulti((path, consumer) -> { + var file1 = path.toFile(); + Criteria key = null; + try { + var date = ((FileTime) Files.getAttribute(path, "creationTime")).toInstant(); + key = new Criteria(date, file1.length(), settings.roundToSecond); + } + catch (IOException ex) { + log.log(Level.SEVERE, null, ex); + res[0] = 2; + } + var found = map.get(key); + if (found != null && !found.isEmpty()) { + found.forEach(item -> { + var relPath1 = file1.getPath().substring(dir1.length() + 1); + var relPath2 = item.path2.substring(dir2.length() + 1); + if (!relPath1.equals(relPath2)) { + var file2 = new File(item.path2); + var d1 = relPath1.substring(0, relPath1.lastIndexOf(file1.getName())); + var d2 = relPath2.substring(0, relPath2.lastIndexOf(file2.getName())); + var out = new Item(item.criteria); + out.select = found.size() <= 1; + if (d1.equals(d2)) { + out.action = bundle.getString("action.rename"); + } + else if (file1.getName().equals(file2.getName())) { + out.action = bundle.getString("action.move"); + } + else { + out.action = bundle.getString("action.both"); + } + out.path1 = file1.getPath(); + out.path2 = item.path2; + consumer.accept(out); + } + }); + } + }) + .filter(item -> item != null) + .forEach(item -> { + items.add(item); + }); + } + catch (URISyntaxException | IOException ex) { + log.log(Level.SEVERE, null, ex); + res[0] = 2; + } + } + + void execute(ArrayList items, String dir1, String dir2, int[] cpt) { + var i = items.iterator(); + while (i.hasNext()) { + var item = i.next(); + if (item.select) { + var relPath2 = item.path2.substring(dir2.length() + 1); + var relPath1 = dir1 + "/" + relPath2; + log.log(Level.INFO, "{0} {1} to {2}", new Object[]{item.action, item.path1, relPath1}); + try { + FileUtils.moveFile(FileUtils.getFile(item.path1), FileUtils.getFile(relPath1)); + cpt[0]++; + } + catch (IOException ex) { + log.log(Level.SEVERE, null, ex); + cpt[1]++; + } + i.remove(); + } + } + } + +} diff --git a/src/main/java/uriot/renmov/Criteria.java b/src/main/java/uriot/renmov/Criteria.java new file mode 100644 index 0000000..98c1dc9 --- /dev/null +++ b/src/main/java/uriot/renmov/Criteria.java @@ -0,0 +1,47 @@ +package uriot.renmov; + +import java.time.Instant; + +public class Criteria { + public Instant date; + public long size; + private final long millis; + + public Criteria(Instant date, long size, boolean roundToSecond) { + this.date = date; + this.size = size; + this.millis = roundToSecond ? date.toEpochMilli() / 1000L : date.toEpochMilli(); + } + + @Override + public int hashCode() { + var hash = 7; + hash = 71 * hash + (int) (this.size ^ (this.size >>> 32)); + hash = 71 * hash + (int) (this.millis ^ (this.millis >>> 32)); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + var other = (Criteria) obj; + if (this.size != other.size) { + return false; + } + return other.millis == millis; + } + + @Override + public String toString() { + return "Criteria{" + "date=" + date + ", size=" + size + '}'; + } + +} diff --git a/src/main/java/uriot/renmov/Item.java b/src/main/java/uriot/renmov/Item.java new file mode 100644 index 0000000..3c956db --- /dev/null +++ b/src/main/java/uriot/renmov/Item.java @@ -0,0 +1,29 @@ +package uriot.renmov; + +import java.time.Instant; + +public class Item { + Criteria criteria; + boolean select; + String path1; + String path2; + String action; + + public Item(Criteria criteria) { + this.criteria = criteria; + select = true; + } + + public static final String[] columnNames = { + "Sél", "Fichier 1", "Fichier 2", "Date", "Taille", "Action" // i18n override + }; + + public static final Class[] columnClasses = { + Boolean.class, String.class, String.class, Instant.class, Long.class, String.class + }; + + @Override + public String toString() { + return "Item{" + "criteria=" + criteria + ", select=" + select + ", path1=" + path1 + ", path2=" + path2 + ", action=" + action + '}'; + } +} diff --git a/src/main/java/uriot/renmov/ItemTableModel.java b/src/main/java/uriot/renmov/ItemTableModel.java new file mode 100644 index 0000000..fbaefdd --- /dev/null +++ b/src/main/java/uriot/renmov/ItemTableModel.java @@ -0,0 +1,66 @@ +package uriot.renmov; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import javax.swing.table.AbstractTableModel; + +public class ItemTableModel extends AbstractTableModel { + + private final ArrayList items; + public int dir1Length; + public int dir2Length; + + public ItemTableModel(ArrayList items) { + this.items = items; + } + + @Override + public int getRowCount() { + return items.size(); + } + + @Override + public int getColumnCount() { + return Item.columnNames.length; + } + + @Override + public String getColumnName(int column) { + return Item.columnNames[column]; + } + + @Override + public Class getColumnClass(int columnIndex) { + return Item.columnClasses[columnIndex]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return columnIndex==0; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + var item = items.get(rowIndex); + return switch (columnIndex) { + case 0 -> item.select; + case 1 -> item.path1.substring(dir1Length); + case 2 -> item.path2.substring(dir2Length); + case 3 -> formatter.format(Date.from(item.criteria.date)); + case 4 -> item.criteria.size; + case 5 -> item.action; + default -> null; + }; + } + private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy/dd/MM HH:mm:ss"); + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + var item = items.get(rowIndex); + switch (columnIndex) { + case 0 -> item.select = (Boolean) value; + } + } + +} diff --git a/src/main/java/uriot/renmov/MainFrame.form b/src/main/java/uriot/renmov/MainFrame.form new file mode 100644 index 0000000..88829e4 --- /dev/null +++ b/src/main/java/uriot/renmov/MainFrame.form @@ -0,0 +1,288 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/uriot/renmov/MainFrame.java b/src/main/java/uriot/renmov/MainFrame.java new file mode 100644 index 0000000..d3e7503 --- /dev/null +++ b/src/main/java/uriot/renmov/MainFrame.java @@ -0,0 +1,542 @@ +package uriot.renmov; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; +import javax.swing.JEditorPane; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.text.html.HTMLEditorKit; + +public class MainFrame extends javax.swing.JFrame { + + private final Logger log = Logger.getLogger("renmov"); + private final ArrayList items = new ArrayList<>(10); + private final Controller controller = new Controller(); + private Settings settings; + private ResourceBundle bundle; + + public MainFrame() { + try (var fis = new FileInputStream("settings.ser"); + var ois = new ObjectInputStream(fis)) { + settings = (Settings) ois.readObject(); + log.log(Level.INFO, "found settings {0}", settings); + } + catch (IOException | ClassNotFoundException ex) { + var currentLocale = Locale.getDefault(); + settings = new Settings(false, false, currentLocale.getLanguage(), null, null); + log.log(Level.INFO, "created settings {0}", settings); + } + controller.log = log; + controller.settings = settings; + updateLogger(); + + initComponents(); + + var itm = new ItemTableModel(items); + itemTable.setModel(itm); + var columnModel = itemTable.getColumnModel(); + columnModel.getColumn(0).setMaxWidth(30); + columnModel.getColumn(3).setPreferredWidth(150); + columnModel.getColumn(3).setMaxWidth(150); + columnModel.getColumn(4).setPreferredWidth(80); + columnModel.getColumn(4).setMaxWidth(100); + columnModel.getColumn(5).setMaxWidth(100); + itemTable.setAutoCreateRowSorter(true); + + localize(settings.language); + + dir1.setText(settings.lastDir1); + dir2.setText(settings.lastDir2); + resetButtons(); + switch (settings.language) { + case "en" -> enLocaleMenuItem.setSelected(true); + case "fr" -> frLocaleMenuItem.setSelected(true); + case "ru" -> ruLocaleMenuItem.setSelected(true); + case "default" -> defLocaleMenuItem.setSelected(true); + } + } + + private void localize(String language) { + var locale = (language.equals("default")) ? Locale.getDefault() : new Locale(language); + bundle = ResourceBundle.getBundle("i18n", locale); + controller.bundle = bundle; + + this.setTitle(bundle.getString("title")); + + fileMenu.setText(bundle.getString("fileMenu")); + exitMenuItem.setText(bundle.getString("exitMenuItem")); + prefMenu.setText(bundle.getString("prefMenu")); + logToFileMenuItem.setText(bundle.getString("logToFileMenuItem")); + roundToSecondMenuItem.setText(bundle.getString("roundToSecondMenuItem")); + enLocaleMenuItem.setText(bundle.getString("enLocaleMenuItem")); + frLocaleMenuItem.setText(bundle.getString("frLocaleMenuItem")); + ruLocaleMenuItem.setText(bundle.getString("ruLocaleMenuItem")); + defLocaleMenuItem.setText(bundle.getString("defLocaleMenuItem")); + helpMenu.setText(bundle.getString("helpMenu")); + aboutMenuItem.setText(bundle.getString("aboutMenuItem")); + + dir1Button.setText(bundle.getString("directory1")); + dir2Button.setText(bundle.getString("directory2")); + dir1Button.setToolTipText(bundle.getString("directory1Tooltip")); + dir2Button.setToolTipText(bundle.getString("directory2Tooltip")); + searchButton.setText(bundle.getString("search")); + executeButton.setText(bundle.getString("execute")); + + itemTable.getColumnModel().getColumn(0).setHeaderValue(bundle.getString("col.sel")); + itemTable.getColumnModel().getColumn(1).setHeaderValue(bundle.getString("col.file1")); + itemTable.getColumnModel().getColumn(2).setHeaderValue(bundle.getString("col.file2")); + itemTable.getColumnModel().getColumn(3).setHeaderValue(bundle.getString("col.date")); + itemTable.getColumnModel().getColumn(4).setHeaderValue(bundle.getString("col.size")); + itemTable.getColumnModel().getColumn(5).setHeaderValue(bundle.getString("col.action")); + itemTable.getTableHeader().resizeAndRepaint(); + } + + private void updateLogger() { + if (settings.logToFile) { + log.info("add log file appender"); + try { + var fileHandler = new FileHandler("renmov.log"); + fileHandler.setFormatter(new SimpleFormatter()); + log.addHandler(fileHandler); + } + catch (IOException | SecurityException ex) { + log.log(Level.SEVERE, "error creating log file handler", ex); + } + } + else { + log.info("remove log file appender"); + Arrays.stream(log.getHandlers()).filter(h -> h instanceof FileHandler).forEach(h -> log.removeHandler(h)); + } + } + + /** + * This method is called from within the constructor to initialize the form. WARNING: Do NOT modify this code. The content of this method is always regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + jPanel = new javax.swing.JPanel(); + dir1 = new javax.swing.JTextField(); + dir1Button = new javax.swing.JButton(); + dir2 = new javax.swing.JTextField(); + dir2Button = new javax.swing.JButton(); + jScrollPane = new javax.swing.JScrollPane(); + itemTable = new javax.swing.JTable(); + searchButton = new javax.swing.JButton(); + executeButton = new javax.swing.JButton(); + result = new javax.swing.JLabel(); + jMenuBar = new javax.swing.JMenuBar(); + fileMenu = new javax.swing.JMenu(); + exitMenuItem = new javax.swing.JMenuItem(); + prefMenu = new javax.swing.JMenu(); + logToFileMenuItem = new javax.swing.JCheckBoxMenuItem(); + roundToSecondMenuItem = new javax.swing.JCheckBoxMenuItem(); + jSeparator1 = new javax.swing.JPopupMenu.Separator(); + enLocaleMenuItem = new javax.swing.JRadioButtonMenuItem(); + frLocaleMenuItem = new javax.swing.JRadioButtonMenuItem(); + ruLocaleMenuItem = new javax.swing.JRadioButtonMenuItem(); + defLocaleMenuItem = new javax.swing.JRadioButtonMenuItem(); + helpMenu = new javax.swing.JMenu(); + aboutMenuItem = new javax.swing.JMenuItem(); + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent evt) { + closeWindow(evt); + } + }); + + jPanel.setLayout(null); + + dir1.setName(""); // NOI18N + dir1.setPreferredSize(new java.awt.Dimension(230, 27)); + + dir1Button.setText("Directory 1"); + dir1Button.setToolTipText("Target directory"); + dir1Button.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chooseDir1(evt); + } + }); + + dir2.setPreferredSize(new java.awt.Dimension(230, 27)); + + dir2Button.setText("Directory 2"); + dir2Button.setToolTipText("Model directory"); + dir2Button.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chooseDir2(evt); + } + }); + + itemTable.setColumnSelectionAllowed(true); + jScrollPane.setViewportView(itemTable); + itemTable.getColumnModel().getSelectionModel().setSelectionMode(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + + searchButton.setText("Search"); + searchButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + search(evt); + } + }); + + executeButton.setText("Execute"); + executeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + execute(evt); + } + }); + + fileMenu.setText("File"); + + exitMenuItem.setText("Exit"); + exitMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + exitMenuItemActionPerformed(evt); + } + }); + fileMenu.add(exitMenuItem); + + jMenuBar.add(fileMenu); + + prefMenu.setText("Preferences"); + + logToFileMenuItem.setSelected(settings.logToFile); + logToFileMenuItem.setText("Log to file"); + logToFileMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + setLogToFile(evt); + } + }); + prefMenu.add(logToFileMenuItem); + + roundToSecondMenuItem.setSelected(settings.roundToSecond); + roundToSecondMenuItem.setText("Round time to second"); + roundToSecondMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + setRoundToSecond(evt); + } + }); + prefMenu.add(roundToSecondMenuItem); + prefMenu.add(jSeparator1); + + enLocaleMenuItem.setText("english"); + enLocaleMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + enLocaleMenuItemActionPerformed(evt); + } + }); + prefMenu.add(enLocaleMenuItem); + + frLocaleMenuItem.setText("french"); + frLocaleMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + frLocaleMenuItemActionPerformed(evt); + } + }); + prefMenu.add(frLocaleMenuItem); + + ruLocaleMenuItem.setText("russian"); + ruLocaleMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + ruLocaleMenuItemActionPerformed(evt); + } + }); + prefMenu.add(ruLocaleMenuItem); + + defLocaleMenuItem.setText("default"); + defLocaleMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + defLocaleMenuItemActionPerformed(evt); + } + }); + prefMenu.add(defLocaleMenuItem); + + jMenuBar.add(prefMenu); + + helpMenu.setText("Help"); + + aboutMenuItem.setText("About"); + aboutMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + aboutMenuItemActionPerformed(evt); + } + }); + helpMenu.add(aboutMenuItem); + + jMenuBar.add(helpMenu); + + setJMenuBar(jMenuBar); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(113, 113, 113)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(19, 19, 19) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(dir2Button, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(dir1Button, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(searchButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(dir1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(dir2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(executeButton) + .addGap(0, 0, Short.MAX_VALUE)))) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap() + .addComponent(jScrollPane))) + .addContainerGap()) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(result, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(426, 426, 426)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(jPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(dir1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(dir1Button)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(dir2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(dir2Button)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(searchButton) + .addComponent(executeButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 267, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(result) + .addGap(28, 28, 28)) + ); + + pack(); + }// //GEN-END:initComponents + + private void chooseDir1(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseDir1 + var fileChooser = new JFileChooser(dir1.getText()); + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + var returnValue = fileChooser.showOpenDialog(null); + if (returnValue == JFileChooser.APPROVE_OPTION) { + var selectedFile = fileChooser.getSelectedFile(); + dir1.setText(selectedFile.getPath()); + } + }//GEN-LAST:event_chooseDir1 + + private void chooseDir2(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseDir2 + var fileChooser = new JFileChooser(dir2.getText()); + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + var returnValue = fileChooser.showOpenDialog(null); + if (returnValue == JFileChooser.APPROVE_OPTION) { + var selectedFile = fileChooser.getSelectedFile(); + dir2.setText(selectedFile.getPath()); + } + }//GEN-LAST:event_chooseDir2 + + private void search(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_search + var res = new int[1]; + controller.search(items, dir1.getText(), dir2.getText(), res); + switch (res[0]) { + case 0 -> { + itemTable.updateUI(); + itemTable.getRowSorter().allRowsChanged(); + ((ItemTableModel) itemTable.getModel()).dir1Length = dir1.getText().length(); + ((ItemTableModel) itemTable.getModel()).dir2Length = dir2.getText().length(); + result.setText(items.size() + bundle.getString("foundResult")); + } + case 1 -> result.setText(bundle.getString("error.dirs")); + case 2 -> result.setText(bundle.getString("error")); + } + + }//GEN-LAST:event_search + + private void execute(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_execute + var cpt = new int[2]; + controller.execute(items, dir1.getText(), dir2.getText(), cpt); + itemTable.updateUI(); + itemTable.getRowSorter().allRowsChanged(); + result.setText(cpt[0] + java.text.MessageFormat.format(bundle.getString("executeResult"), new Object[] {cpt[1]})); + }//GEN-LAST:event_execute + + private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exitMenuItemActionPerformed + settings.lastDir1 = dir1.getText(); + settings.lastDir2 = dir2.getText(); + try (var fos = new FileOutputStream("settings.ser"); + var oos = new ObjectOutputStream(fos)) { + oos.writeObject(settings); + log.info("settings saved"); + } + catch (IOException ex) { + log.log(Level.SEVERE, "error writing settings", ex); + } + System.exit(0); + }//GEN-LAST:event_exitMenuItemActionPerformed + + private void aboutMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_aboutMenuItemActionPerformed + var jEditorPane = new JEditorPane(); + jEditorPane.setEditable(false); + var scrollPane = new JScrollPane(jEditorPane); + var kit = new HTMLEditorKit(); + jEditorPane.setEditorKit(kit); +// StyleSheet styleSheet = kit.getStyleSheet(); +// styleSheet.addRule("body {color:#000; font-family:times; margin: 4px; }"); +// styleSheet.addRule("h1 {color: blue;}"); +// styleSheet.addRule("pre {font : 10px monaco; color : black; background-color : #fafafa; }"); + var htmlString = "

Help not found !

"; + var suffix = (settings.language.equals("default")) ? Locale.getDefault().getLanguage() : settings.language; + var helpRes = this.getClass().getResource("/about_"+suffix+".html"); + if (helpRes == null) { + helpRes = this.getClass().getResource("/about.html"); + } + var htmlFile = new File(helpRes.getFile()); + try { + var l = Files.readAllLines(htmlFile.toPath(), Charset.defaultCharset()); + var sb = new StringBuilder(); + l.forEach(s -> sb.append(s)); + htmlString = sb.toString(); + } + catch (IOException ex) { + log.log(Level.SEVERE, "help not found", ex); + } + var doc = kit.createDefaultDocument(); + jEditorPane.setDocument(doc); + jEditorPane.setText(htmlString); + var j = new JFrame(bundle.getString("aboutMenuItem")); + j.getContentPane().add(scrollPane, BorderLayout.CENTER); + j.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + j.setSize(new Dimension(640,480)); //j.pack(); pack it, if you prefer + j.setLocationRelativeTo(null); // center the jframe, then make it visible + j.setVisible(true); + }//GEN-LAST:event_aboutMenuItemActionPerformed + + private void setLogToFile(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_setLogToFile + settings.logToFile = logToFileMenuItem.getState(); + updateLogger(); + }//GEN-LAST:event_setLogToFile + + private void setRoundToSecond(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_setRoundToSecond + settings.roundToSecond = roundToSecondMenuItem.getState(); + }//GEN-LAST:event_setRoundToSecond + + private void closeWindow(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeWindow + exitMenuItemActionPerformed(null); + }//GEN-LAST:event_closeWindow + private void resetButtons() { + enLocaleMenuItem.setSelected(false); + frLocaleMenuItem.setSelected(false); + ruLocaleMenuItem.setSelected(false); + defLocaleMenuItem.setSelected(false); + } + private void ruLocaleMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_ruLocaleMenuItemActionPerformed + resetButtons(); + ruLocaleMenuItem.setSelected(true); + settings.language = "ru"; + localize(settings.language); + }//GEN-LAST:event_ruLocaleMenuItemActionPerformed + + private void frLocaleMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_frLocaleMenuItemActionPerformed + resetButtons(); + frLocaleMenuItem.setSelected(true); + settings.language = "fr"; + localize(settings.language); + }//GEN-LAST:event_frLocaleMenuItemActionPerformed + + private void enLocaleMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enLocaleMenuItemActionPerformed + resetButtons(); + enLocaleMenuItem.setSelected(true); + settings.language = "en"; + localize(settings.language); + }//GEN-LAST:event_enLocaleMenuItemActionPerformed + + private void defLocaleMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_defLocaleMenuItemActionPerformed + resetButtons(); + defLocaleMenuItem.setSelected(true); + settings.language = "default"; + localize(settings.language); + }//GEN-LAST:event_defLocaleMenuItemActionPerformed + + public static void main(String args[]) { + // + /* Set the Nimbus look and feel */ +// try { +// for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) { +// if ("Nimbus".equals(info.getName())) { +// javax.swing.UIManager.setLookAndFeel(info.getClassName()); +// break; +// } +// } +// } +// catch (ClassNotFoundException | InstantiationException | IllegalAccessException | javax.swing.UnsupportedLookAndFeelException ex) { +// java.util.logging.Logger.getLogger(MainFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); +// } + // + + // + + /* Create and display the form */ + java.awt.EventQueue.invokeLater(() -> { + var f = new MainFrame(); + f.setLocation(0, 0); + f.setSize(1000, 600); + f.setVisible(true); + }); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JMenuItem aboutMenuItem; + private javax.swing.JRadioButtonMenuItem defLocaleMenuItem; + public javax.swing.JTextField dir1; + private javax.swing.JButton dir1Button; + public javax.swing.JTextField dir2; + private javax.swing.JButton dir2Button; + private javax.swing.JRadioButtonMenuItem enLocaleMenuItem; + private javax.swing.JButton executeButton; + private javax.swing.JMenuItem exitMenuItem; + private javax.swing.JMenu fileMenu; + private javax.swing.JRadioButtonMenuItem frLocaleMenuItem; + private javax.swing.JMenu helpMenu; + public javax.swing.JTable itemTable; + private javax.swing.JMenuBar jMenuBar; + private javax.swing.JPanel jPanel; + private javax.swing.JScrollPane jScrollPane; + private javax.swing.JPopupMenu.Separator jSeparator1; + private javax.swing.JCheckBoxMenuItem logToFileMenuItem; + private javax.swing.JMenu prefMenu; + public javax.swing.JLabel result; + private javax.swing.JCheckBoxMenuItem roundToSecondMenuItem; + private javax.swing.JRadioButtonMenuItem ruLocaleMenuItem; + private javax.swing.JButton searchButton; + // End of variables declaration//GEN-END:variables +} diff --git a/src/main/java/uriot/renmov/Settings.java b/src/main/java/uriot/renmov/Settings.java new file mode 100644 index 0000000..ef13904 --- /dev/null +++ b/src/main/java/uriot/renmov/Settings.java @@ -0,0 +1,25 @@ +package uriot.renmov; + +import java.io.Serializable; + +public class Settings implements Serializable { + Boolean logToFile; + Boolean roundToSecond; + String language; + String lastDir1; + String lastDir2; + + public Settings(Boolean logToFile, Boolean roundToSecond, String language, String lastDir1, String lastDir2) { + this.logToFile = logToFile; + this.roundToSecond = roundToSecond; + this.language = language; + this.lastDir1 = lastDir1; + this.lastDir2 = lastDir2; + } + + @Override + public String toString() { + return "Settings{" + "logToFile=" + logToFile + ", roundToSecond=" + roundToSecond + ", language=" + language + ", lastDir1=" + lastDir1 + ", lastDir2=" + lastDir2 + '}'; + } + +} diff --git a/src/main/resources/about.html b/src/main/resources/about.html new file mode 100644 index 0000000..549a44d --- /dev/null +++ b/src/main/resources/about.html @@ -0,0 +1,9 @@ + + +

The purpose of this small application is to synchronize two initially similar directories, but one of which has renamed and moved files, to apply the same modifications to the second.

+

The typical use case is that of a backup on 'dir1' of 'dir2'; after classifying files in dir2 (renaming and moving files in sub-directories), if we make a new backup, we are offered copy+deletes on dir1 which is inefficient.

+

The application detects the actions performed on dir2, relying on the size and the date of update (assumed unchanged by the classification), and proposes to perform the same renaming/moving operations on the target directory dir1.

+

Operating mode : choose directories 1 (target that will be modified) and 2 (model), press 'search'; select the items to update then press 'execute'.

+

A setting allows to log the actions in a file, to round the times to the second and to choose the language.

+ + diff --git a/src/main/resources/about_fr.html b/src/main/resources/about_fr.html new file mode 100644 index 0000000..691d51e --- /dev/null +++ b/src/main/resources/about_fr.html @@ -0,0 +1,9 @@ + + +

Le but de cette petite application est de synchroniser deux répertoires initialement semblables, mais dont l'un a subi des renommages et déplacements pour appliquer les mêmes modifications au second.

+

Le cas d'usage typique est celui d'un backup sur 'dir1' de 'dir2'; après classement de fichiers dans dir2 (renommage et déplacements de fichiers dans des sous-répertoires), si l'on fait un nouveau backup, on se voit proposer des copies+suppressions sur dir1 ce qui est inefficace.

+

L'application détecte les actions effectuées sur dir2, s'appuyant la taille et la date de mise à jour (supposée inchangée par le classement), et propose d'effectuer les mêmes opérations de renommage/déplacement sur le répertoire cible dir1.

+

Mode opératoire : choisir les répertoires 1 (cible qui sera modifiée) et 2 (modèle), appuyer sur 'chercher'; sélectionner les items à mettre à jour puis appuuer sur 'exécuter'.

+

Un paramétrage permet de logger les actions dans un fichier, d'arrondir les temps à la seconde et de choisir la langue.

+ + diff --git a/src/main/resources/i18n_en.properties b/src/main/resources/i18n_en.properties new file mode 100644 index 0000000..63fd273 --- /dev/null +++ b/src/main/resources/i18n_en.properties @@ -0,0 +1,31 @@ +title=RenMov (Rename/Move) +executeResult=\ items updated; {0} errors +foundResult=\ items found +execute=Execute +search=Search +directory1=Directory 1 +directory1Tooltip=Target directory +directory2=Directory 2 +directory2Tooltip=Model directory +col.sel=Sel +col.file1=File 1 +col.file2=File 2 +col.date=Date +col.size=Size +col.action=Action +action.rename=RENAME +action.move=MOVE +action.both=BOTH +fileMenu=File +exitMenuItem=Quit +prefMenu=Settings +logToFileMenuItem=Log to file +roundToSecondMenuItem=Round to second +enLocaleMenuItem=Globish +frLocaleMenuItem=French +ruLocaleMenuItem=Russian +defLocaleMenuItem=Default +helpMenu=Help +aboutMenuItem=About +error.dirs=A directory was not found +error=An error occured, look at log diff --git a/src/main/resources/i18n_fr.properties b/src/main/resources/i18n_fr.properties new file mode 100644 index 0000000..33f3778 --- /dev/null +++ b/src/main/resources/i18n_fr.properties @@ -0,0 +1,31 @@ +title=RenMov (renomme/d\u00e9place) +executeResult=\ \u00e9l\u00e9ments modifi\u00e9s; {0} erreurs +foundResult=\ \u00e9l\u00e9ments trouv\u00e9s +execute=Effectuer +search=Rechercher +directory1=R\u00e9pertoire 1 +directory1Tooltip=R\u00e9pertoire cible +directory2=R\u00e9pertoire 2 +directory2Tooltip=R\u00e9pertoire mod\u00e8le +col.sel=S\u00e9l +col.file1=Fichier 1 +col.file2=Fichier 2 +col.date=Date +col.size=Taille +col.action=Action +action.rename=RENOMME +action.move=DEPLACE +action.both=REN+DEPL +fileMenu=Fichier +exitMenuItem=Quitter +prefMenu=Pr\u00e9f\u00e9rences +logToFileMenuItem=Log dans fichier +roundToSecondMenuItem=Arrondi \u00e0 la seconde +enLocaleMenuItem=Globish +frLocaleMenuItem=Fran\u00e7ais +ruLocaleMenuItem=Russe +defLocaleMenuItem=D\u00e9faut +helpMenu=Aide +aboutMenuItem=A propos +error.dirs=Un des r\u00e9pertoires est inexistant +error=Erreur de traitement, cf log