Skip to content

Commit 660266c

Browse files
committed
iRODS: TEST THIS. IF WORKING, SPLIT IT INTO CLEAN COMMITS
1 parent ccae480 commit 660266c

File tree

9 files changed

+341
-142
lines changed

9 files changed

+341
-142
lines changed

defaults/src/main/resources/default.properties

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,3 +776,16 @@ deepbox.listing.box.trash=false
776776
# 1 min
777777
deepbox.download.interrupt.ms=60000
778778
deepbox.download.interval.ms=0=50 2=200 5=500 15=2000
779+
780+
# iRODS configuration
781+
# -------------------
782+
irods.client_negotiation_policy=CS_NEG_REFUSE
783+
# Local files exceeding the following size will trigger use of parallel transfer.
784+
# Normally defaults to 32MB in iRODS clients.
785+
irods.parallel_transfer.size_threshold=33554432
786+
# The number of threads used for parallel transfer.
787+
irods.parallel_transfer.thread_count=3
788+
# The size of the buffer used for reading during parallel transfer.
789+
irods.parallel_transfer.rbuffer_size=4194304
790+
# The size of the buffer used for writing during parallel transfer.
791+
irods.parallel_transfer.wbuffer_size=4194304

irods/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<dependency>
2828
<groupId>org.irods</groupId>
2929
<artifactId>irods4j</artifactId>
30-
<version>0.3.0-java8-SNAPSHOT</version>
30+
<version>0.3.0-java8</version>
3131
</dependency>
3232
<dependency>
3333
<groupId>ch.cyberduck</groupId>

irods/src/main/java/ch/cyberduck/core/irods/IRODSChunkWorker.java

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
* GNU General Public License for more details.
1616
*/
1717

18-
import org.irods.irods4j.high_level.connection.IRODSConnectionPool;
1918
import org.irods.irods4j.high_level.io.IRODSDataObjectInputStream;
2019
import org.irods.irods4j.high_level.io.IRODSDataObjectOutputStream;
2120
import org.irods.irods4j.high_level.io.IRODSDataObjectStream;
@@ -29,15 +28,13 @@
2928

3029
public class IRODSChunkWorker implements Runnable {
3130

32-
private final IRODSConnectionPool.PoolConnection conn;
3331
private final InputStream in;
3432
private final OutputStream out;
3533
private final long offset;
3634
private final long chunkSize;
3735
private final byte[] buffer;
3836

39-
public IRODSChunkWorker(IRODSConnectionPool.PoolConnection conn, InputStream in, OutputStream out, long offset, long chunkSize, int bufferSize) throws IOException, IRODSException {
40-
this.conn = conn;
37+
public IRODSChunkWorker(InputStream in, OutputStream out, long offset, long chunkSize, int bufferSize) {
4138
this.in = in;
4239
this.out = out;
4340
this.offset = offset;
@@ -67,16 +64,6 @@ public void run() {
6764
catch(IOException | IRODSException e) {
6865
// TODO Log error
6966
}
70-
finally {
71-
try {
72-
in.close();
73-
}
74-
catch(Exception e) { /* Ignored */ }
75-
try {
76-
out.close();
77-
}
78-
catch(Exception e) { /* Ignored */ }
79-
}
8067
}
8168

8269
private void seek(InputStream in) throws IRODSException, IOException {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package ch.cyberduck.core.irods;
2+
3+
/*
4+
* Copyright (c) 2002-2025 iterate GmbH. All rights reserved.
5+
* https://cyberduck.io/
6+
*
7+
* This program is free software; you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*/
17+
18+
import org.apache.commons.lang3.StringUtils;
19+
import org.irods.irods4j.authentication.AuthPlugin;
20+
import org.irods.irods4j.authentication.NativeAuthPlugin;
21+
import org.irods.irods4j.authentication.PamInteractiveAuthPlugin;
22+
import org.irods.irods4j.authentication.PamPasswordAuthPlugin;
23+
import org.irods.irods4j.high_level.connection.IRODSConnectionPool;
24+
import org.irods.irods4j.high_level.connection.QualifiedUsername;
25+
import org.irods.irods4j.low_level.api.IRODSApi;
26+
import org.irods.irods4j.low_level.api.IRODSException;
27+
28+
import java.io.IOException;
29+
30+
final class IRODSConnectionUtils {
31+
32+
public static void startIRODSConnectionPool(IRODSSession session, IRODSConnectionPool connPool) throws IRODSException, IOException {
33+
String host = session.getHost().getHostname();
34+
int port = session.getHost().getPort();
35+
String zone = session.getRegion();
36+
String username = session.getHost().getCredentials().getUsername();
37+
String password = session.getHost().getCredentials().getPassword();
38+
39+
connPool.start(
40+
host,
41+
port,
42+
new QualifiedUsername(username, zone),
43+
conn -> {
44+
try {
45+
final String authScheme = StringUtils.defaultIfBlank(session.getHost().getProtocol().getAuthorization(), "native");
46+
AuthPlugin plugin = null;
47+
if("native".equals(authScheme)) {
48+
plugin = new NativeAuthPlugin();
49+
}
50+
else if("pam_password".equals(authScheme)) {
51+
plugin = new PamPasswordAuthPlugin(true);
52+
}
53+
else if("pam_interactive".equals(authScheme)) {
54+
plugin = new PamInteractiveAuthPlugin(true);
55+
}
56+
else {
57+
throw new IllegalArgumentException(String.format("Authentication scheme not recognized: %s", authScheme));
58+
}
59+
IRODSApi.rcAuthenticateClient(conn, plugin, password);
60+
return true;
61+
}
62+
catch(Exception e) {
63+
return false;
64+
}
65+
});
66+
}
67+
}

irods/src/main/java/ch/cyberduck/core/irods/IRODSDownloadFeature.java

Lines changed: 91 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,21 @@
2424
import ch.cyberduck.core.features.Read;
2525
import ch.cyberduck.core.io.BandwidthThrottle;
2626
import ch.cyberduck.core.io.StreamListener;
27+
import ch.cyberduck.core.preferences.HostPreferencesFactory;
28+
import ch.cyberduck.core.preferences.PreferencesReader;
2729
import ch.cyberduck.core.transfer.TransferStatus;
2830

2931
import org.irods.irods4j.high_level.connection.IRODSConnectionPool;
3032
import org.irods.irods4j.high_level.connection.IRODSConnectionPool.PoolConnection;
31-
import org.irods.irods4j.high_level.connection.QualifiedUsername;
3233
import org.irods.irods4j.high_level.io.IRODSDataObjectInputStream;
3334
import org.irods.irods4j.high_level.vfs.IRODSFilesystem;
34-
import org.irods.irods4j.low_level.api.IRODSApi;
3535
import org.irods.irods4j.low_level.api.IRODSApi.RcComm;
3636

37-
import java.io.BufferedOutputStream;
3837
import java.io.FileOutputStream;
39-
import java.io.RandomAccessFile;
38+
import java.io.OutputStream;
4039
import java.nio.file.Files;
4140
import java.nio.file.Paths;
41+
import java.nio.file.StandardOpenOption;
4242
import java.util.ArrayList;
4343
import java.util.List;
4444
import java.util.concurrent.ExecutorService;
@@ -49,8 +49,6 @@
4949
public class IRODSDownloadFeature implements Download {
5050

5151
private final IRODSSession session;
52-
private final int numThread = 3; // TODO Make this configurable
53-
private static final int BUFFER_SIZE = 4 * 1024 * 1024; // TODO Make configurable
5452

5553
public IRODSDownloadFeature(final IRODSSession session) {
5654
this.session = session;
@@ -61,6 +59,8 @@ public void download(final Path file, final Local local, final BandwidthThrottle
6159
final StreamListener listener, final TransferStatus status,
6260
final ConnectionCallback callback) throws BackgroundException {
6361
try {
62+
final PreferencesReader preferences = HostPreferencesFactory.get(session.getHost());
63+
6464
final RcComm primaryConn = session.getClient().getRcComm();
6565
final String logicalPath = file.getAbsolute();
6666

@@ -72,9 +72,8 @@ public void download(final Path file, final Local local, final BandwidthThrottle
7272

7373
// Transfer the bytes over multiple connections if the size of the data object
7474
// exceeds a certain threshold - e.g. 32MB.
75-
// TODO Consider making this configurable.
76-
if(dataObjectSize < 32 * 1024 * 1024) {
77-
byte[] buffer = new byte[4 * 1024 * 1024];
75+
if(dataObjectSize < 32 * 1024 * 1024) { //preferences.getInteger("irods.parallel_transfer.size_threshold")) {
76+
byte[] buffer = new byte[4 * 1024 * 1024]; //preferences.getInteger("irods.parallel_transfer.rbuffer_size")];
7877

7978
try(IRODSDataObjectInputStream in = new IRODSDataObjectInputStream(primaryConn, logicalPath);
8079
FileOutputStream out = new FileOutputStream(local.getAbsolute())) {
@@ -92,58 +91,77 @@ public void download(final Path file, final Local local, final BandwidthThrottle
9291
// The data object is larger than the threshold so use parallel transfer.
9392
//
9493

95-
// Step 1: Get replica token & number via primary stream
96-
try(IRODSDataObjectInputStream primary = new IRODSDataObjectInputStream(primaryConn, logicalPath)) {
97-
final String replicaToken = primary.getReplicaToken();
98-
final long replicaNumber = primary.getReplicaNumber();
99-
100-
// Step 2: Setup connection pool
101-
try(final IRODSConnectionPool pool = new IRODSConnectionPool(numThread)) {
102-
pool.start(
103-
session.getHost().getHostname(),
104-
session.getHost().getPort(),
105-
new QualifiedUsername(session.getHost().getCredentials().getUsername(), session.getRegion()),
106-
conn -> {
107-
try {
108-
// TODO Needs to take the value of the Authorization profile value.
109-
IRODSApi.rcAuthenticateClient(conn, "native", session.getHost().getCredentials().getPassword());
110-
return true;
111-
}
112-
catch(Exception e) {
113-
return false;
114-
}
115-
});
116-
117-
final long chunkSize = dataObjectSize / numThread;
118-
final long remainChunkSize = dataObjectSize % numThread;
119-
120-
// Step 3: Create empty target file
121-
try(RandomAccessFile out = new RandomAccessFile(local.getAbsolute(), "rw")) {
122-
out.setLength(dataObjectSize);
123-
}
94+
// TODO Clamp the value so that users do not specify something ridiculous.
95+
final int threadCount = 3; //preferences.getInteger("irods.parallel_transfer.thread_count");
96+
final ExecutorService executor = Executors.newFixedThreadPool(threadCount);
97+
98+
final long chunkSize = dataObjectSize / threadCount;
99+
final long remainingBytes = dataObjectSize % threadCount;
100+
101+
final List<IRODSDataObjectInputStream> secondaryIrodsStreams = new ArrayList<>();
102+
final List<OutputStream> localFileStreams = new ArrayList<>();
103+
104+
// Open the primary iRODS input stream.
105+
// TODO Needs to pass the target resource name if provided.
106+
try(IRODSDataObjectInputStream primaryStream = new IRODSDataObjectInputStream(primaryConn, logicalPath)) {
107+
// Initialize connections for secondary streams.
108+
try(IRODSConnectionPool pool = new IRODSConnectionPool(threadCount - 1)) {
109+
IRODSConnectionUtils.startIRODSConnectionPool(session, pool);
124110

125-
// Step 4: Parallel readers
126-
final ExecutorService executor = Executors.newFixedThreadPool(numThread);
111+
// Holds handles to tasks running on the thread pool. This allows us to wait for
112+
// all tasks to complete before shutting down everything.
127113
List<Future<?>> tasks = new ArrayList<>();
128-
for(int i = 0; i < numThread; i++) {
129-
final PoolConnection conn = pool.getConnection();
130-
tasks.add(executor.submit(new IRODSChunkWorker(
131-
conn,
132-
new IRODSDataObjectInputStream(conn.getRcComm(), replicaToken, replicaNumber),
133-
new FileOutputStream(local.getAbsolute()),
134-
i * chunkSize,
135-
(numThread - 1 == i) ? chunkSize + remainChunkSize : chunkSize,
136-
BUFFER_SIZE
137-
)));
138-
}
139114

140-
for(Future<?> task : tasks) {
141-
task.get();
142-
}
115+
// Open the first output stream for the local file and store it for processing.
116+
// This also guarantees the target file exists and is empty (i.e. truncated to zero
117+
// if it exists).
118+
final java.nio.file.Path localFilePath = Paths.get(local.getAbsolute());
119+
localFileStreams.add(Files.newOutputStream(localFilePath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
120+
121+
// Launch the first IO task.
122+
tasks.add(executor.submit(new IRODSChunkWorker(
123+
primaryStream,
124+
localFileStreams.get(0),
125+
0,
126+
chunkSize,
127+
4 * 1024 * 1024//preferences.getInteger("irods.parallel_transfer.rbuffer_size")
128+
)));
129+
130+
try {
131+
// Launch remaining IO tasks.
132+
for(int i = 1; i < threadCount; ++i) {
133+
PoolConnection conn = pool.getConnection();
134+
secondaryIrodsStreams.add(new IRODSDataObjectInputStream(conn.getRcComm(), logicalPath));
135+
localFileStreams.add(Files.newOutputStream(localFilePath, StandardOpenOption.WRITE));
136+
tasks.add(executor.submit(new IRODSChunkWorker(
137+
secondaryIrodsStreams.get(secondaryIrodsStreams.size() - 1),
138+
localFileStreams.get(localFileStreams.size() - 1),
139+
i * chunkSize,
140+
(threadCount - 1 == i) ? chunkSize + remainingBytes : chunkSize,
141+
4 * 1024 * 1024//preferences.getInteger("irods.parallel_transfer.rbuffer_size")
142+
)));
143+
}
143144

144-
executor.shutdown();
145+
// Wait for all tasks on the thread pool to complete.
146+
for(Future<?> task : tasks) {
147+
try {
148+
task.get();
149+
}
150+
catch(Exception e) { /* Ignored */ }
151+
}
152+
}
153+
finally {
154+
closeInputStreams(secondaryIrodsStreams);
155+
}
145156
}
146157
}
158+
finally {
159+
closeOutputStreams(localFileStreams);
160+
}
161+
162+
executor.shutdown();
163+
// TODO Make this configurable.
164+
executor.awaitTermination(5, TimeUnit.SECONDS);
147165
}
148166
catch(Exception e) {
149167
throw new IRODSExceptionMappingService().map("Download {0} failed", e);
@@ -159,4 +177,22 @@ public boolean offset(final Path file) {
159177
public Download withReader(final Read reader) {
160178
return this;
161179
}
180+
181+
private static void closeInputStreams(List<IRODSDataObjectInputStream> streams) {
182+
streams.forEach(in -> {
183+
try {
184+
in.close();
185+
}
186+
catch(Exception e) { /* Ignored */ }
187+
});
188+
}
189+
190+
private static void closeOutputStreams(List<OutputStream> streams) {
191+
streams.forEach(out -> {
192+
try {
193+
out.close();
194+
}
195+
catch(Exception e) { /* Ignored */ }
196+
});
197+
}
162198
}

irods/src/main/java/ch/cyberduck/core/irods/IRODSExceptionMappingService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.logging.log4j.Logger;
2323

2424
public class IRODSExceptionMappingService extends AbstractExceptionMappingService<Exception> {
25+
2526
private static final Logger log = LogManager.getLogger(IRODSExceptionMappingService.class);
2627

2728
@Override
@@ -32,5 +33,4 @@ public BackgroundException map(final Exception e) {
3233
this.append(buffer, e.getMessage());
3334
return this.wrap(e, buffer);
3435
}
35-
3636
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package ch.cyberduck.core.irods;
2+
3+
/*
4+
* Copyright (c) 2002-2025 iterate GmbH. All rights reserved.
5+
* https://cyberduck.io/
6+
*
7+
* This program is free software; you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*/
17+
18+
public final class IRODSIntegerUtils {
19+
20+
static <T extends Comparable<T>> T clamp(T value, T low, T high) {
21+
if(value.compareTo(low) < 0) {
22+
return low;
23+
}
24+
25+
if(value.compareTo(high) > 0) {
26+
return high;
27+
}
28+
29+
return value;
30+
}
31+
32+
}

0 commit comments

Comments
 (0)