Skip to content

Commit c7b3cb4

Browse files
authored
GH-10162: The full file path for PersistentAcceptOnceFileListFilter (#10420)
GH-10162: The full file path for `PersistentAcceptOnceFileListFilter` Fixes: #10162 The `AbstractPersistentAcceptOnceFileListFilter` may cause a problem with entries in the `MetadataStore` when the same file name is used from different directories. Let's imagine a scenario when remote directories are created every day with respective timestamp, e.g. today `20250828`, tomorrow `20250829`. The files in those directories are based on customer names: `customer1.xml`, `customer2.xml` etc. Since most of the `AbstractPersistentAcceptOnceFileListFilter` implementations do just something like this: `return file.getName();` that would lead to the same key for the `MetadataStore` operations. * Fix `SmbPersistentAcceptOnceFileListFilter` to use `SmbFile.getUncPath()` as a notation of the full path file * Fix `SftpPersistentAcceptOnceFileListFilter` to use `SftpClient.DirEntry.getLongFilename()` if it is not empty. * Fix `SftpSession.list()` to populated the mentioned `longFileName` into a new `SftpClient.DirEntry` instance based on just fetched * Introduce an `EnhancedFTPFile` adapter to delegate to the original `FTPFile` and populate an additional `longFileName` property * Check for this new class in the `FtpPersistentAcceptOnceFileListFilter` to extract its `longFileName` property * Fix Checkstyle violation: remove unused import * Fix typos in docs * Improve `FtpSession.list()` to use a new array instead of `LinkedList` * Also, use `.` as a working directory placeholder and always populate a long file name
1 parent de8b2b3 commit c7b3cb4

File tree

11 files changed

+279
-26
lines changed

11 files changed

+279
-26
lines changed

spring-integration-ftp/src/main/java/org/springframework/integration/ftp/filters/FtpPersistentAcceptOnceFileListFilter.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@
1919
import org.apache.commons.net.ftp.FTPFile;
2020

2121
import org.springframework.integration.file.filters.AbstractPersistentAcceptOnceFileListFilter;
22+
import org.springframework.integration.ftp.session.EnhancedFTPFile;
2223
import org.springframework.integration.metadata.ConcurrentMetadataStore;
24+
import org.springframework.util.StringUtils;
2325

2426
/**
2527
* Persistent file list filter using the server's file timestamp to detect if we've already
2628
* 'seen' this file.
2729
*
2830
* @author Gary Russell
31+
* @author Artem Bilan
2932
*
3033
* @since 3.0
3134
*
@@ -43,6 +46,13 @@ protected long modified(FTPFile file) {
4346

4447
@Override
4548
protected String fileName(FTPFile file) {
49+
if (file instanceof EnhancedFTPFile enhancedFTPFile) {
50+
String longFileName = enhancedFTPFile.getLongFileName();
51+
if (StringUtils.hasText(longFileName)) {
52+
return longFileName;
53+
}
54+
}
55+
4656
return file.getName();
4757
}
4858

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
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+
* https://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+
17+
package org.springframework.integration.ftp.session;
18+
19+
import java.io.Serial;
20+
import java.time.Instant;
21+
import java.util.Calendar;
22+
23+
import org.apache.commons.net.ftp.FTPFile;
24+
import org.jspecify.annotations.Nullable;
25+
26+
/**
27+
* The {@link FTPFile} extension to provide additional information,
28+
* e.g., long file name with directory included.
29+
* The instance of this class is based on the original {@link FTPFile}
30+
* with delegation from all the methods.
31+
*
32+
* @author Artem Bilan
33+
*
34+
* @since 7.0
35+
*/
36+
public class EnhancedFTPFile extends FTPFile {
37+
38+
@Serial
39+
private static final long serialVersionUID = 9010790363003271996L;
40+
41+
private final FTPFile delegate;
42+
43+
private @Nullable String longFileName;
44+
45+
public EnhancedFTPFile(FTPFile delegate) {
46+
this.delegate = delegate;
47+
}
48+
49+
@Override
50+
public String getGroup() {
51+
return this.delegate.getGroup();
52+
}
53+
54+
@Override
55+
public int getHardLinkCount() {
56+
return this.delegate.getHardLinkCount();
57+
}
58+
59+
@Override
60+
public String getLink() {
61+
return this.delegate.getLink();
62+
}
63+
64+
@Override
65+
public String getName() {
66+
return this.delegate.getName();
67+
}
68+
69+
@Override
70+
public String getRawListing() {
71+
return this.delegate.getRawListing();
72+
}
73+
74+
@Override
75+
public long getSize() {
76+
return this.delegate.getSize();
77+
}
78+
79+
@Override
80+
public Calendar getTimestamp() {
81+
return this.delegate.getTimestamp();
82+
}
83+
84+
@Override
85+
public Instant getTimestampInstant() {
86+
return this.delegate.getTimestampInstant();
87+
}
88+
89+
@Override
90+
public int getType() {
91+
return this.delegate.getType();
92+
}
93+
94+
@Override
95+
public String getUser() {
96+
return this.delegate.getUser();
97+
}
98+
99+
@Override
100+
public boolean hasPermission(int access, int permission) {
101+
return this.delegate.hasPermission(access, permission);
102+
}
103+
104+
@Override
105+
public boolean isDirectory() {
106+
return this.delegate.isDirectory();
107+
}
108+
109+
@Override
110+
public boolean isFile() {
111+
return this.delegate.isFile();
112+
}
113+
114+
@Override
115+
public boolean isSymbolicLink() {
116+
return this.delegate.isSymbolicLink();
117+
}
118+
119+
@Override
120+
public boolean isUnknown() {
121+
return this.delegate.isUnknown();
122+
}
123+
124+
@Override
125+
public boolean isValid() {
126+
return this.delegate.isValid();
127+
}
128+
129+
@Override
130+
public void setGroup(String group) {
131+
this.delegate.setGroup(group);
132+
}
133+
134+
@Override
135+
public void setHardLinkCount(int hardLinkCount) {
136+
this.delegate.setHardLinkCount(hardLinkCount);
137+
}
138+
139+
@Override
140+
public void setLink(String link) {
141+
this.delegate.setLink(link);
142+
}
143+
144+
@Override
145+
public void setName(String name) {
146+
this.delegate.setName(name);
147+
}
148+
149+
@Override
150+
public void setPermission(int access, int permission, boolean value) {
151+
this.delegate.setPermission(access, permission, value);
152+
}
153+
154+
@Override
155+
public void setRawListing(String rawListing) {
156+
this.delegate.setRawListing(rawListing);
157+
}
158+
159+
@Override
160+
public void setSize(long size) {
161+
this.delegate.setSize(size);
162+
}
163+
164+
@Override
165+
public void setTimestamp(Calendar calendar) {
166+
this.delegate.setTimestamp(calendar);
167+
}
168+
169+
@Override
170+
public void setType(int type) {
171+
this.delegate.setType(type);
172+
}
173+
174+
@Override
175+
public void setUser(String user) {
176+
this.delegate.setUser(user);
177+
}
178+
179+
@Override
180+
public String toFormattedString() {
181+
return this.delegate.toFormattedString();
182+
}
183+
184+
@Override
185+
public String toFormattedString(String timezone) {
186+
return this.delegate.toFormattedString(timezone);
187+
}
188+
189+
@Override
190+
public String toString() {
191+
return this.delegate.toString();
192+
}
193+
194+
public @Nullable String getLongFileName() {
195+
return this.longFileName;
196+
}
197+
198+
public void setLongFileName(@Nullable String longFileName) {
199+
this.longFileName = longFileName;
200+
}
201+
202+
}

spring-integration-ftp/src/main/java/org/springframework/integration/ftp/session/FtpSession.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.integration.file.remote.session.Session;
3232
import org.springframework.util.Assert;
3333
import org.springframework.util.ObjectUtils;
34+
import org.springframework.util.StringUtils;
3435

3536
/**
3637
* Implementation of {@link Session} for FTP.
@@ -71,7 +72,17 @@ public boolean remove(String path) throws IOException {
7172

7273
@Override
7374
public FTPFile[] list(@Nullable String path) throws IOException {
74-
return this.client.listFiles(path);
75+
FTPFile[] ftpFiles = this.client.listFiles(path);
76+
String remoteDir = StringUtils.hasText(path) ? path : ".";
77+
EnhancedFTPFile[] enhancedFtpFiles = new EnhancedFTPFile[ftpFiles.length];
78+
for (int i = 0; i < ftpFiles.length; i++) {
79+
FTPFile ftpFile = ftpFiles[i];
80+
EnhancedFTPFile enhancedFTPFile = new EnhancedFTPFile(ftpFile);
81+
enhancedFTPFile.setLongFileName(remoteDir + '/' + ftpFile.getName());
82+
enhancedFtpFiles[i] = enhancedFTPFile;
83+
}
84+
85+
return enhancedFtpFiles;
7586
}
7687

7788
@Override

spring-integration-ftp/src/test/java/org/springframework/integration/ftp/inbound/FtpInboundRemoteFileSystemSynchronizerTests.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Collection;
2525
import java.util.List;
2626
import java.util.Map;
27+
import java.util.Properties;
2728

2829
import org.apache.commons.net.ftp.FTPClient;
2930
import org.apache.commons.net.ftp.FTPFile;
@@ -69,7 +70,7 @@
6970
*/
7071
public class FtpInboundRemoteFileSystemSynchronizerTests implements TestApplicationContextAware {
7172

72-
private static FTPClient ftpClient = mock(FTPClient.class);
73+
private static final FTPClient ftpClient = mock(FTPClient.class);
7374

7475
@BeforeEach
7576
@AfterEach
@@ -85,7 +86,7 @@ public void testCopyFileToLocalDir() throws Exception {
8586
TestFtpSessionFactory ftpSessionFactory = new TestFtpSessionFactory();
8687
ftpSessionFactory.setUsername("kermit");
8788
ftpSessionFactory.setPassword("frog");
88-
ftpSessionFactory.setHost("foo.com");
89+
ftpSessionFactory.setHost("some-host.com");
8990
FtpInboundFileSynchronizer synchronizer = spy(new FtpInboundFileSynchronizer(ftpSessionFactory));
9091
synchronizer.setDeleteRemoteFiles(true);
9192
synchronizer.setPreserveTimestamp(true);
@@ -97,7 +98,7 @@ public void testCopyFileToLocalDir() throws Exception {
9798
store.setBaseDirectory("test");
9899
store.afterPropertiesSet();
99100
FtpPersistentAcceptOnceFileListFilter persistFilter =
100-
new FtpPersistentAcceptOnceFileListFilter(store, "foo");
101+
new FtpPersistentAcceptOnceFileListFilter(store, "someKey/");
101102
List<FileListFilter<FTPFile>> filters = new ArrayList<>();
102103
filters.add(persistFilter);
103104
filters.add(patternFilter);
@@ -166,6 +167,10 @@ public void testCopyFileToLocalDir() throws Exception {
166167

167168
Map<?, ?> metadata = TestUtils.getPropertyValue(remoteFileMetadataStore, "metadata", Map.class);
168169
assertThat(metadata).isEmpty();
170+
171+
Properties metadataProperties = TestUtils.getPropertyValue(store, "metadata", Properties.class);
172+
metadataProperties.stringPropertyNames()
173+
.forEach(name -> assertThat(name).startsWith("someKey/remote-test-dir/"));
169174
}
170175

171176
@Test

spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import org.springframework.integration.ftp.server.PathRemovedEvent;
6969
import org.springframework.integration.ftp.server.SessionClosedEvent;
7070
import org.springframework.integration.ftp.server.SessionOpenedEvent;
71+
import org.springframework.integration.ftp.session.EnhancedFTPFile;
7172
import org.springframework.integration.ftp.session.FtpRemoteFileTemplate;
7273
import org.springframework.integration.support.MessageBuilder;
7374
import org.springframework.integration.support.PartialSuccessException;
@@ -516,7 +517,7 @@ public void testMgetPartial() throws Exception {
516517
FTPFile[] files = (FTPFile[]) invocation.callRealMethod();
517518
// add an extra file where the get will fail
518519
files = Arrays.copyOf(files, files.length + 1);
519-
FTPFile bogusFile = new FTPFile();
520+
FTPFile bogusFile = new EnhancedFTPFile(new FTPFile());
520521
bogusFile.setName("bogus.txt");
521522
files[files.length - 1] = bogusFile;
522523
return files;
@@ -542,7 +543,7 @@ public void testMgetRecursivePartial() throws Exception {
542543
FTPFile[] files = (FTPFile[]) invocation.callRealMethod();
543544
// add an extra file where the get will fail
544545
files = Arrays.copyOf(files, files.length + 1);
545-
FTPFile bogusFile = new FTPFile();
546+
FTPFile bogusFile = new EnhancedFTPFile(new FTPFile());
546547
bogusFile.setName("bogus.txt");
547548
bogusFile.setTimestamp(Calendar.getInstance());
548549
files[files.length - 1] = bogusFile;

spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpPersistentAcceptOnceFileListFilter.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.springframework.integration.file.filters.AbstractPersistentAcceptOnceFileListFilter;
2222
import org.springframework.integration.metadata.ConcurrentMetadataStore;
23+
import org.springframework.util.StringUtils;
2324

2425
/**
2526
* Persistent file list filter using the server's file timestamp to detect if we've already
@@ -46,7 +47,8 @@ protected long modified(SftpClient.DirEntry file) {
4647

4748
@Override
4849
protected String fileName(SftpClient.DirEntry file) {
49-
return file.getFilename();
50+
String longFilename = file.getLongFilename();
51+
return StringUtils.hasText(longFilename) ? longFilename : file.getFilename();
5052
}
5153

5254
@Override

spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpSession.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,17 @@ public Stream<SftpClient.DirEntry> doList(@Nullable String path) throws IOExcept
126126
}
127127
}
128128
remoteDir = normalizePath(remoteDir);
129-
return StreamSupport.stream(this.sftpClient.readDir(remoteDir).spliterator(), false)
130-
.filter((entry) -> !isPattern || PatternMatchUtils.simpleMatch(remoteFile, entry.getFilename()));
129+
String remoteDirToUse = remoteDir;
130+
return StreamSupport.stream(this.sftpClient.readDir(remoteDirToUse).spliterator(), false)
131+
.filter((entry) -> !isPattern || PatternMatchUtils.simpleMatch(remoteFile, entry.getFilename()))
132+
.map((entry) -> {
133+
String longFilename = entry.getLongFilename();
134+
if (StringUtils.hasText(longFilename)) {
135+
return entry;
136+
}
137+
String filename = entry.getFilename();
138+
return new SftpClient.DirEntry(filename, remoteDirToUse + '/' + filename, entry.getAttributes());
139+
});
131140
}
132141

133142
@Override

0 commit comments

Comments
 (0)