Skip to content

Commit 624fe83

Browse files
Support premature termination of listing (#928)
* Support premature termination of listing * Added license header + small refactor --------- Co-authored-by: Jeroen van Erp <[email protected]>
1 parent 81d77d2 commit 624fe83

File tree

6 files changed

+196
-32
lines changed

6 files changed

+196
-32
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (C)2009 - SSHJ Contributors
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 com.hierynomus.sshj.sftp;
17+
18+
import com.hierynomus.sshj.sftp.RemoteResourceSelector.Result;
19+
import net.schmizz.sshj.sftp.RemoteResourceFilter;
20+
21+
public class RemoteResourceFilterConverter {
22+
23+
public static RemoteResourceSelector selectorFrom(RemoteResourceFilter filter) {
24+
if (filter == null) {
25+
return RemoteResourceSelector.ALL;
26+
}
27+
28+
return resource -> filter.accept(resource) ? Result.ACCEPT : Result.CONTINUE;
29+
}
30+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (C)2009 - SSHJ Contributors
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 com.hierynomus.sshj.sftp;
17+
18+
import net.schmizz.sshj.sftp.RemoteResourceInfo;
19+
20+
public interface RemoteResourceSelector {
21+
public static RemoteResourceSelector ALL = new RemoteResourceSelector() {
22+
@Override
23+
public Result select(RemoteResourceInfo resource) {
24+
return Result.ACCEPT;
25+
}
26+
};
27+
28+
enum Result {
29+
/**
30+
* Accept the remote resource and add it to the result.
31+
*/
32+
ACCEPT,
33+
34+
/**
35+
* Do not add the remote resource to the result and continue with the next.
36+
*/
37+
CONTINUE,
38+
39+
/**
40+
* Do not add the remote resource to the result and stop further execution.
41+
*/
42+
BREAK;
43+
}
44+
45+
/**
46+
* Decide whether the remote resource should be included in the result and whether execution should continue.
47+
*/
48+
Result select(RemoteResourceInfo resource);
49+
}

src/main/java/net/schmizz/sshj/sftp/RemoteDirectory.java

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
*/
1616
package net.schmizz.sshj.sftp;
1717

18+
import com.hierynomus.sshj.sftp.RemoteResourceSelector;
1819
import net.schmizz.sshj.sftp.Response.StatusCode;
1920

2021
import java.io.IOException;
2122
import java.util.LinkedList;
2223
import java.util.List;
2324
import java.util.concurrent.TimeUnit;
2425

26+
import static com.hierynomus.sshj.sftp.RemoteResourceFilterConverter.selectorFrom;
27+
2528
public class RemoteDirectory
2629
extends RemoteResource {
2730

@@ -31,37 +34,55 @@ public RemoteDirectory(SFTPEngine requester, String path, byte[] handle) {
3134

3235
public List<RemoteResourceInfo> scan(RemoteResourceFilter filter)
3336
throws IOException {
34-
List<RemoteResourceInfo> rri = new LinkedList<RemoteResourceInfo>();
35-
// TODO: Remove GOTO!
36-
loop:
37-
for (; ; ) {
38-
final Response res = requester.request(newRequest(PacketType.READDIR))
37+
return scan(selectorFrom(filter));
38+
}
39+
40+
public List<RemoteResourceInfo> scan(RemoteResourceSelector selector)
41+
throws IOException {
42+
if (selector == null) {
43+
selector = RemoteResourceSelector.ALL;
44+
}
45+
46+
List<RemoteResourceInfo> remoteResourceInfos = new LinkedList<>();
47+
48+
while (true) {
49+
final Response response = requester.request(newRequest(PacketType.READDIR))
3950
.retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
40-
switch (res.getType()) {
4151

52+
switch (response.getType()) {
4253
case NAME:
43-
final int count = res.readUInt32AsInt();
54+
final int count = response.readUInt32AsInt();
4455
for (int i = 0; i < count; i++) {
45-
final String name = res.readString(requester.sub.getRemoteCharset());
46-
res.readString(); // long name - IGNORED - shdve never been in the protocol
47-
final FileAttributes attrs = res.readFileAttributes();
56+
final String name = response.readString(requester.sub.getRemoteCharset());
57+
response.readString(); // long name - IGNORED - shdve never been in the protocol
58+
final FileAttributes attrs = response.readFileAttributes();
4859
final PathComponents comps = requester.getPathHelper().getComponents(path, name);
4960
final RemoteResourceInfo inf = new RemoteResourceInfo(comps, attrs);
50-
if (!(".".equals(name) || "..".equals(name)) && (filter == null || filter.accept(inf))) {
51-
rri.add(inf);
61+
62+
if (".".equals(name) || "..".equals(name)) {
63+
continue;
64+
}
65+
66+
final RemoteResourceSelector.Result selectionResult = selector.select(inf);
67+
switch (selectionResult) {
68+
case ACCEPT:
69+
remoteResourceInfos.add(inf);
70+
break;
71+
case CONTINUE:
72+
continue;
73+
case BREAK:
74+
return remoteResourceInfos;
5275
}
5376
}
5477
break;
5578

5679
case STATUS:
57-
res.ensureStatusIs(StatusCode.EOF);
58-
break loop;
80+
response.ensureStatusIs(StatusCode.EOF);
81+
return remoteResourceInfos;
5982

6083
default:
61-
throw new SFTPException("Unexpected packet: " + res.getType());
84+
throw new SFTPException("Unexpected packet: " + response.getType());
6285
}
6386
}
64-
return rri;
6587
}
66-
6788
}

src/main/java/net/schmizz/sshj/sftp/SFTPClient.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package net.schmizz.sshj.sftp;
1717

18+
import com.hierynomus.sshj.sftp.RemoteResourceSelector;
1819
import net.schmizz.sshj.connection.channel.direct.SessionFactory;
1920
import net.schmizz.sshj.xfer.FilePermission;
2021
import net.schmizz.sshj.xfer.LocalDestFile;
@@ -25,6 +26,8 @@
2526
import java.io.IOException;
2627
import java.util.*;
2728

29+
import static com.hierynomus.sshj.sftp.RemoteResourceFilterConverter.selectorFrom;
30+
2831
public class SFTPClient
2932
implements Closeable {
3033

@@ -57,16 +60,18 @@ public SFTPFileTransfer getFileTransfer() {
5760

5861
public List<RemoteResourceInfo> ls(String path)
5962
throws IOException {
60-
return ls(path, null);
63+
return ls(path, RemoteResourceSelector.ALL);
6164
}
6265

6366
public List<RemoteResourceInfo> ls(String path, RemoteResourceFilter filter)
6467
throws IOException {
65-
final RemoteDirectory dir = engine.openDir(path);
66-
try {
67-
return dir.scan(filter);
68-
} finally {
69-
dir.close();
68+
return ls(path, selectorFrom(filter));
69+
}
70+
71+
public List<RemoteResourceInfo> ls(String path, RemoteResourceSelector selector)
72+
throws IOException {
73+
try (RemoteDirectory dir = engine.openDir(path)) {
74+
return dir.scan(selector == null ? RemoteResourceSelector.ALL : selector);
7075
}
7176
}
7277

src/main/java/net/schmizz/sshj/sftp/StatefulSFTPClient.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package net.schmizz.sshj.sftp;
1717

18+
import com.hierynomus.sshj.sftp.RemoteResourceSelector;
1819
import net.schmizz.sshj.connection.channel.direct.SessionFactory;
1920
import net.schmizz.sshj.xfer.LocalDestFile;
2021
import net.schmizz.sshj.xfer.LocalSourceFile;
@@ -23,6 +24,8 @@
2324
import java.util.List;
2425
import java.util.Set;
2526

27+
import static com.hierynomus.sshj.sftp.RemoteResourceFilterConverter.selectorFrom;
28+
2629
public class StatefulSFTPClient
2730
extends SFTPClient {
2831

@@ -57,7 +60,7 @@ public synchronized void cd(String dirname)
5760

5861
public synchronized List<RemoteResourceInfo> ls()
5962
throws IOException {
60-
return ls(cwd, null);
63+
return ls(cwd, RemoteResourceSelector.ALL);
6164
}
6265

6366
public synchronized List<RemoteResourceInfo> ls(RemoteResourceFilter filter)
@@ -70,20 +73,21 @@ public synchronized String pwd()
7073
return super.canonicalize(cwd);
7174
}
7275

73-
@Override
7476
public List<RemoteResourceInfo> ls(String path)
7577
throws IOException {
76-
return ls(path, null);
78+
return ls(path, RemoteResourceSelector.ALL);
7779
}
7880

79-
@Override
8081
public List<RemoteResourceInfo> ls(String path, RemoteResourceFilter filter)
8182
throws IOException {
82-
final RemoteDirectory dir = getSFTPEngine().openDir(cwdify(path));
83-
try {
84-
return dir.scan(filter);
85-
} finally {
86-
dir.close();
83+
return ls(path, selectorFrom(filter));
84+
}
85+
86+
@Override
87+
public List<RemoteResourceInfo> ls(String path, RemoteResourceSelector selector)
88+
throws IOException {
89+
try (RemoteDirectory dir = getSFTPEngine().openDir(cwdify(path))) {
90+
return dir.scan(selector == null ? RemoteResourceSelector.ALL : selector);
8791
}
8892
}
8993

src/test/groovy/com/hierynomus/sshj/sftp/SFTPClientSpec.groovy

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.hierynomus.sshj.test.SshServerExtension
1919
import com.hierynomus.sshj.test.util.FileUtil
2020
import net.schmizz.sshj.SSHClient
2121
import net.schmizz.sshj.sftp.FileMode
22+
import net.schmizz.sshj.sftp.RemoteResourceInfo
2223
import net.schmizz.sshj.sftp.SFTPClient
2324
import org.junit.jupiter.api.extension.RegisterExtension
2425
import spock.lang.Specification
@@ -206,6 +207,60 @@ class SFTPClientSpec extends Specification {
206207
attrs.type == FileMode.Type.DIRECTORY
207208
}
208209
210+
def "should support premature termination of listing"() {
211+
given:
212+
SSHClient sshClient = fixture.setupConnectedDefaultClient()
213+
sshClient.authPassword("test", "test")
214+
SFTPClient sftpClient = sshClient.newSFTPClient()
215+
216+
final Path source = Files.createDirectory(temp.resolve("source")).toAbsolutePath()
217+
final Path destination = Files.createDirectory(temp.resolve("destination")).toAbsolutePath()
218+
final Path firstFile = Files.writeString(source.resolve("a_first.txt"), "first")
219+
final Path secondFile = Files.writeString(source.resolve("b_second.txt"), "second")
220+
final Path thirdFile = Files.writeString(source.resolve("c_third.txt"), "third")
221+
final Path fourthFile = Files.writeString(source.resolve("d_fourth.txt"), "fourth")
222+
sftpClient.put(firstFile.toString(), destination.resolve(firstFile.fileName).toString())
223+
sftpClient.put(secondFile.toString(), destination.resolve(secondFile.fileName).toString())
224+
sftpClient.put(thirdFile.toString(), destination.resolve(thirdFile.fileName).toString())
225+
sftpClient.put(fourthFile.toString(), destination.resolve(fourthFile.fileName).toString())
226+
227+
def filesListed = 0
228+
RemoteResourceInfo expectedFile = null
229+
RemoteResourceSelector limitingSelector = new RemoteResourceSelector() {
230+
@Override
231+
RemoteResourceSelector.Result select(RemoteResourceInfo resource) {
232+
filesListed += 1
233+
234+
switch(filesListed) {
235+
case 1:
236+
return RemoteResourceSelector.Result.CONTINUE
237+
case 2:
238+
expectedFile = resource
239+
return RemoteResourceSelector.Result.ACCEPT
240+
case 3:
241+
return RemoteResourceSelector.Result.BREAK
242+
default:
243+
throw new AssertionError((Object) "Should NOT select any more resources")
244+
}
245+
}
246+
}
247+
248+
when:
249+
def listingResult = sftpClient
250+
.ls(destination.toString(), limitingSelector);
251+
252+
then:
253+
// first should be skipped by CONTINUE
254+
listingResult.contains(expectedFile) // second should be included by ACCEPT
255+
// third should be skipped by BREAK
256+
// fourth should be skipped by preceding BREAK
257+
listingResult.size() == 1
258+
259+
cleanup:
260+
sftpClient.close()
261+
sshClient.disconnect()
262+
}
263+
209264
private void doUpload(File src, File dest) throws IOException {
210265
SSHClient sshClient = fixture.setupConnectedDefaultClient()
211266
sshClient.authPassword("test", "test")

0 commit comments

Comments
 (0)