View Javadoc
1   /*
2    * Copyright 2026, Aiki IT, FotoRenamer
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package de.aikiit.fotorenamer.image;
17  
18  import org.apache.logging.log4j.Level;
19  import org.apache.logging.log4j.LogManager;
20  import org.apache.logging.log4j.core.LogEvent;
21  import org.apache.logging.log4j.core.Logger;
22  import org.apache.logging.log4j.core.appender.AbstractAppender;
23  import org.apache.logging.log4j.core.config.Property;
24  import org.junit.jupiter.api.Test;
25  import org.junit.jupiter.api.io.TempDir;
26  
27  import java.nio.file.FileAlreadyExistsException;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.concurrent.CopyOnWriteArrayList;
33  
34  import static org.assertj.core.api.Assertions.assertThat;
35  
36  class RemoveExifPrefixRenamerTest {
37  
38      @TempDir
39      Path directory;
40  
41      @Test
42      void roundTripRestoresOriginalFilenameAndContents() throws Exception {
43          Path original = copyImage("holiday.dat");
44          byte[] content = Files.readAllBytes(original);
45          var forward = new CreationDateFromExifImageRenamer(directory.toString());
46          assertThat(forward.renameFiles().renamed()).isEqualTo(1);
47          Path prefixed = directory.resolve("20110130_131102_holiday.dat");
48          assertThat(prefixed).exists();
49          assertThat(original).doesNotExist();
50  
51          var reverse = new RemoveExifPrefixRenamer(directory.toString());
52          // Construction no longer starts a worker or performs any renaming.
53          assertThat(prefixed).exists();
54          assertThat(reverse.renameFiles().renamed()).isEqualTo(1);
55          assertThat(prefixed).doesNotExist();
56          assertThat(Files.readAllBytes(original)).containsExactly(content);
57      }
58  
59      @Test
60      void supportsLegacyPrefixesAndUnicodeNamesWithoutExtensions() throws Exception {
61          Path legacy = copyImage("20110130_1311_legacy.jpg");
62          Path unicode = copyImage("20110130_131102_Urlaub am See äß");
63          byte[] legacyContent = Files.readAllBytes(legacy);
64          byte[] unicodeContent = Files.readAllBytes(unicode);
65          var renamer = new RemoveExifPrefixRenamer(directory.toString());
66          assertThat(renamer.renameFiles()).isEqualTo(new RenameResult(2, 2, 2, 0, 0, false));
67          assertThat(Files.readAllBytes(directory.resolve("legacy.jpg"))).containsExactly(legacyContent);
68          assertThat(Files.readAllBytes(directory.resolve("Urlaub am See äß"))).containsExactly(unicodeContent);
69      }
70  
71      @Test
72      void preservesUnchangedNamesAndEmptyTargetsAndReportsProgress() throws Exception {
73          List<String> names = List.of("plain.jpg", "photo_20110130_131102_test.jpg",
74                  "20110130_13110_invalid.jpg", "20110130_131102_");
75          for (String name : names) {
76              copyImage(name);
77          }
78          var renamer = new RemoveExifPrefixRenamer(directory.toString());
79          List<Path> processed = new ArrayList<>();
80          assertThat(renamer.renameFiles(progress -> processed.add(progress.file()))).isEqualTo(new RenameResult(4, 4, 0, 4, 0, false));
81          assertThat(processed).hasSameSizeAs(names);
82          for (String name : names) {
83              assertThat(directory.resolve(name)).as(name).exists();
84          }
85      }
86  
87      @Test
88      void removesOnlyOneLeadingPrefix() throws Exception {
89          Path image = copyImage("20110130_131102_20110129_1200_photo.bin");
90          byte[] original = Files.readAllBytes(image);
91          var renamer = new RemoveExifPrefixRenamer(directory.toString());
92          assertThat(renamer.renameFiles().renamed()).isEqualTo(1);
93          assertThat(Files.readAllBytes(directory.resolve("20110129_1200_photo.bin"))).containsExactly(original);
94      }
95  
96      @Test
97      void ignoresNonImagesAndSubdirectoriesEvenWithMatchingPrefixes() throws Exception {
98          Path text = Files.writeString(directory.resolve("20110130_131102_notes.jpg"), "not an image");
99          Path folder = Files.createDirectory(directory.resolve("20110130_131102_folder"));
100         copyImage("20110130_131102_photo");
101         var renamer = new RemoveExifPrefixRenamer(directory.toString());
102         assertThat(renamer.renameFiles().renamed()).isEqualTo(1);
103         assertThat(directory.resolve("photo")).exists();
104         assertThat(Files.readString(text)).isEqualTo("not an image");
105         assertThat(folder).isDirectory();
106     }
107 
108     @Test
109     void preservesExistingTargetsLogsCauseAndContinuesWithOtherImages() throws Exception {
110         Path source = copyImage("20110130_131102_photo.dat");
111         byte[] original = Files.readAllBytes(source);
112         Path target = Files.writeString(directory.resolve("photo.dat"), "keep this file");
113         copyImage("20110130_131102_other.dat");
114         var renamer = new RemoveExifPrefixRenamer(directory.toString());
115         Logger logger = (Logger) LogManager.getLogger(AbstractImageRenamer.class);
116         List<LogEvent> events = new CopyOnWriteArrayList<>();
117         AbstractAppender appender = new AbstractAppender("reverse-errors", null, null, false, Property.EMPTY_ARRAY) {
118             @Override
119             public void append(LogEvent event) {
120                 events.add(event.toImmutable());
121             }
122         };
123         appender.start();
124         logger.addAppender(appender);
125         try {
126             assertThat(renamer.renameFiles()).isEqualTo(new RenameResult(2, 2, 1, 0, 1, false));
127             assertThat(events.stream().anyMatch(event -> event.getLevel() == Level.ERROR
128                     && event.getThrown() instanceof FileAlreadyExistsException
129                     && event.getMessage().getFormattedMessage().contains(source.toString())
130                     && event.getMessage().getFormattedMessage().contains(target.toString()))).isTrue();
131         } finally {
132             logger.removeAppender(appender);
133             appender.stop();
134         }
135         assertThat(Files.readAllBytes(source)).containsExactly(original);
136         assertThat(Files.readString(target)).isEqualTo("keep this file");
137         assertThat(directory.resolve("other.dat")).exists();
138     }
139 
140     @Test
141     void sourceReplacedByDirectoryIsLeftUntouched() throws Exception {
142         Path source = copyImage("20110130_131102_replaced");
143         var renamer = new RemoveExifPrefixRenamer(directory.toString());
144         Files.delete(source);
145         Files.createDirectory(source);
146         List<Path> processed = new ArrayList<>();
147         assertThat(renamer.renameFiles(progress -> processed.add(progress.file()))).isEqualTo(new RenameResult(1, 1, 0, 1, 0, false));
148         assertThat(processed).containsExactly(source);
149         assertThat(source).isDirectory();
150         assertThat(directory.resolve("replaced")).doesNotExist();
151     }
152 
153     @Test
154     void missingSourceDoesNotAbortRemainingMoves() throws Exception {
155         Path missing = copyImage("20110130_131102_missing");
156         copyImage("20110130_131102_existing");
157         var renamer = new RemoveExifPrefixRenamer(directory.toString());
158         Files.delete(missing);
159         assertThat(renamer.renameFiles()).isEqualTo(new RenameResult(2, 2, 1, 0, 1, false));
160         assertThat(directory.resolve("existing")).exists();
161         assertThat(directory.resolve("missing")).doesNotExist();
162     }
163 
164     private Path copyImage(String name) throws Exception {
165         Path target = directory.resolve(name);
166         try (var input = getClass().getResourceAsStream("/data/IMG_7559_mini.JPG")) {
167             assertThat(input).isNotNull();
168             Files.copy(input, target);
169         }
170         return target;
171     }
172 }