Skip to content
30 changes: 29 additions & 1 deletion src/main/java/org/metricshub/ssh/SshClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@
.append("\t")
.append(fileAttributes.atime.toString())
.append("\t-\t")
.append(Integer.toString(fileAttributes.permissions & 0000777, 8))

Check warning on line 330 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value
.append("\t")
.append(fileAttributes.size.toString())
.append("\t-\t")
Expand All @@ -346,6 +346,34 @@
return pslFileResult.toString();
}

/**
* Returns the file size.
*
* @param filePath Path to the file on the remote system
* @return the file size
* @throws IOException if the file does not exist
*/
public long fileSize(final String filePath) throws IOException {
SFTPv3Client sftpClient = null;
try {
// Sanity check
checkIfAuthenticated();

// Create the SFTP client
sftpClient = new SFTPv3Client(sshConnection);

// Read the file attributes
final SFTPv3FileAttributes attributes = sftpClient.stat(filePath);

return attributes.size;
} finally {
// Deallocate
if (sftpClient != null) {
sftpClient.close();
}
}
}

private StringBuilder listSubDirectory(
SFTPv3Client sftpClient,
String remoteDirectoryPath,
Expand Down Expand Up @@ -374,17 +402,17 @@
SFTPv3FileAttributes fileAttributes = file.attributes;
String filePath = remoteDirectoryPath + "/" + filename;

if ((fileAttributes.permissions & 0120000) == 0120000) {

Check warning on line 405 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value

Check warning on line 405 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value
// Symbolic link
continue;
}

// CHECKSTYLE:OFF
if (
((fileAttributes.permissions & 0100000) == 0100000) ||

Check warning on line 412 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value

Check warning on line 412 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value
((fileAttributes.permissions & 0060000) == 0060000) ||

Check warning on line 413 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value

Check warning on line 413 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value
((fileAttributes.permissions & 0020000) == 0020000) ||

Check warning on line 414 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value

Check warning on line 414 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value
((fileAttributes.permissions & 0140000) == 0140000)

Check warning on line 415 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value

Check warning on line 415 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value
) {
// Regular/Block/Character/Socket files
final Matcher m = fileMaskPattern.matcher(filename);
Expand All @@ -401,12 +429,12 @@
}
// CHECKSTYLE:ON

if ((fileAttributes.permissions & 0040000) == 0040000) {

Check warning on line 432 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value

Check warning on line 432 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone AvoidUsingOctalValues

Do not start a literal by 0 unless its an octal value
// Directory
if (includeSubfolders) {
resultBuilder =
listSubDirectory(sftpClient, filePath, fileMaskPattern, includeSubfolders, depth, resultBuilder);
}

Check warning on line 437 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Design CollapsibleIfStatements

This if statement could be combined with its parent
}
}
}
Expand Down Expand Up @@ -541,7 +569,7 @@
sftpClient.close();

// Metricshub Collection format
return out.toString();

Check warning on line 572 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / spotbugs

DM_DEFAULT_ENCODING

Found reliance on default encoding in org.metricshub.ssh.SshClient.readFile(String, Long, Integer): java.io.ByteArrayOutputStream.toString()
Raw output
 Found a call to a method which will perform a byte to String (or String to byte) conversion, and will assume that the default platform encoding is suitable. This will cause the application behavior to vary between platforms. Use an alternative API and specify a charset name or Charset object explicitly.
}

/**
Expand Down Expand Up @@ -611,24 +639,24 @@
/**
* Whether the command was successful or not
*/
public boolean success = true;

Check warning on line 642 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / spotbugs

URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD

Unread public/protected field: org.metricshub.ssh.SshClient$CommandResult.success
Raw output
 This field is never read. The field is public or protected, so perhaps it is intended to be used with classes not seen as part of the analysis. If not, consider removing it from the class.

/**
* How much time was taken by the execution itself (not counting the
* connection time), in seconds
*/
public float executionTime = 0;

Check warning on line 648 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / spotbugs

URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD

Unread public/protected field: org.metricshub.ssh.SshClient$CommandResult.executionTime
Raw output
 This field is never read. The field is public or protected, so perhaps it is intended to be used with classes not seen as part of the analysis. If not, consider removing it from the class.

/**
* The exit code (status) returned by the command (process return code).
* <code>null</code> if unsupported by the remote platform.
*/
public Integer exitStatus = null;

Check warning on line 654 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / spotbugs

URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD

Unread public/protected field: org.metricshub.ssh.SshClient$CommandResult.exitStatus
Raw output
 This field is never read. The field is public or protected, so perhaps it is intended to be used with classes not seen as part of the analysis. If not, consider removing it from the class.

/**
* The result of the command (stdout and stderr is merged into result)
*/
public String result = "";

Check warning on line 659 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / spotbugs

URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD

Unread public/protected field: org.metricshub.ssh.SshClient$CommandResult.result
Raw output
 This field is never read. The field is public or protected, so perhaps it is intended to be used with classes not seen as part of the analysis. If not, consider removing it from the class.
}

/**
Expand Down Expand Up @@ -712,7 +740,7 @@
// We completed in time

// Execution time (in seconds)
commandResult.executionTime = (currentTime - startTime) / 1000;

Check warning on line 743 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / spotbugs

ICAST_IDIV_CAST_TO_DOUBLE

Integral division result cast to double or float in org.metricshub.ssh.SshClient.executeCommand(String, int)
Raw output
This code casts the result of an integral division (e.g., int or long division) operation to double or float. Doing division on integers truncates the result to the integer value closest to zero. The fact that the result was cast to double suggests that this precision should have been retained. What was probably meant was to cast one or both of the operands to double before performing the division. Here is an example:

int x = 2;
int y = 5;
// Wrong: yields result 0.0
double value1 = x / y;

// Right: yields result 0.4
double value2 = x / (double) y;

// Read exit status, when available
waitForCondition = sshSession.waitForCondition(ChannelCondition.EXIT_STATUS, 5000);
Expand Down Expand Up @@ -744,7 +772,7 @@
openTerminal();

// Pipe specified InputStream to SSH's stdin -- use a separate thread
BufferedReader inputReader = new BufferedReader(new InputStreamReader(in));

Check warning on line 775 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / spotbugs

DM_DEFAULT_ENCODING

Found reliance on default encoding in org.metricshub.ssh.SshClient.interactiveSession(InputStream, OutputStream): new java.io.InputStreamReader(InputStream)
Raw output
 Found a call to a method which will perform a byte to String (or String to byte) conversion, and will assume that the default platform encoding is suitable. This will cause the application behavior to vary between platforms. Use an alternative API and specify a charset name or Charset object explicitly.
OutputStream outputWriter = sshSession.getStdin();
Thread stdinPipeThread = new Thread() {
@Override
Expand All @@ -752,12 +780,12 @@
try {
String line;
while ((line = inputReader.readLine()) != null) {
outputWriter.write(line.getBytes());

Check warning on line 783 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / spotbugs

DM_DEFAULT_ENCODING

Found reliance on default encoding in org.metricshub.ssh.SshClient$2.run(): String.getBytes()
Raw output
 Found a call to a method which will perform a byte to String (or String to byte) conversion, and will assume that the default platform encoding is suitable. This will cause the application behavior to vary between platforms. Use an alternative API and specify a charset name or Charset object explicitly.
outputWriter.write('\n');
}
} catch (Exception e) {
// Things ended up badly. Exit thread.
}

Check warning on line 788 in src/main/java/org/metricshub/ssh/SshClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone EmptyCatchBlock

Avoid empty catch blocks
// End of the input stream. We need to exit.
// Let's close the session so the main thread exits nicely.
sshSession.close();
Expand Down Expand Up @@ -1009,7 +1037,7 @@
* Check if already authenticate.
*/
void checkIfAuthenticated() {
if (!getSshConnection().isAuthenticationComplete()) {
if (getSshConnection() == null || !getSshConnection().isAuthenticationComplete()) {
throw new IllegalStateException("Authentication is required first");
}
}
Expand Down
95 changes: 95 additions & 0 deletions src/test/java/org/metricshub/ssh/SSHClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

import com.trilead.ssh2.ChannelCondition;
import com.trilead.ssh2.Connection;
import com.trilead.ssh2.SFTPv3Client;
import com.trilead.ssh2.SFTPv3FileAttributes;
import com.trilead.ssh2.Session;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Optional;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;
import org.mockito.Mockito;

class SSHClientTest {
Expand Down Expand Up @@ -525,4 +529,95 @@ void testRead() throws Exception {
Assertions.assertEquals(Optional.of("Hello WorldError"), sshClient.read(0, 5));
}
}

@Test
void testFileSize() throws Exception {
final Connection sshConnection = Mockito.mock(Connection.class);
final String filePath = "/path/to/file.txt";

// Case not Connected
try (final SshClient sshClient = Mockito.spy(new SshClient(HOSTNAME))) {
Mockito.verify(sshClient, Mockito.never()).checkIfAuthenticated();

Assertions.assertThrows(IllegalStateException.class, () -> sshClient.fileSize(filePath));
}

// Case not authenticated
try (final SshClient sshClient = Mockito.spy(new SshClient(HOSTNAME))) {
Mockito.doReturn(sshConnection).when(sshClient).getSshConnection();
Mockito.doReturn(false).when(sshConnection).isAuthenticationComplete();

Assertions.assertThrows(IllegalStateException.class, () -> sshClient.fileSize(filePath));
}

// Case file does not exist (IOException)
try (
final SshClient sshClient = Mockito.spy(new SshClient(HOSTNAME));
final MockedConstruction<SFTPv3Client> mockedConstruction = Mockito.mockConstruction(
SFTPv3Client.class,
(mock, context) -> {
Mockito.when(mock.stat(filePath)).thenThrow(new IOException("File not found"));
}
)
) {
Mockito.doReturn(sshConnection).when(sshClient).getSshConnection();
Mockito.doReturn(true).when(sshConnection).isAuthenticationComplete();

Assertions.assertThrows(IOException.class, () -> sshClient.fileSize(filePath));
}

// Case file size is 0
try (
final SshClient sshClient = Mockito.spy(new SshClient(HOSTNAME));
final MockedConstruction<SFTPv3Client> mockedConstruction = Mockito.mockConstruction(
SFTPv3Client.class,
(mock, context) -> {
final SFTPv3FileAttributes attributes = new SFTPv3FileAttributes();
attributes.size = 0L;
Mockito.when(mock.stat(filePath)).thenReturn(attributes);
}
)
) {
Mockito.doReturn(sshConnection).when(sshClient).getSshConnection();
Mockito.doReturn(true).when(sshConnection).isAuthenticationComplete();

Assertions.assertEquals(0L, sshClient.fileSize(filePath));
}

// Case file size is small (100 bytes)
try (
final SshClient sshClient = Mockito.spy(new SshClient(HOSTNAME));
final MockedConstruction<SFTPv3Client> mockedConstruction = Mockito.mockConstruction(
SFTPv3Client.class,
(mock, context) -> {
final SFTPv3FileAttributes attributes = new SFTPv3FileAttributes();
attributes.size = 100L;
Mockito.when(mock.stat(filePath)).thenReturn(attributes);
}
)
) {
Mockito.doReturn(sshConnection).when(sshClient).getSshConnection();
Mockito.doReturn(true).when(sshConnection).isAuthenticationComplete();

Assertions.assertEquals(100L, sshClient.fileSize(filePath));
}

// Case file size is large (1GB)
try (
final SshClient sshClient = Mockito.spy(new SshClient(HOSTNAME));
final MockedConstruction<SFTPv3Client> mockedConstruction = Mockito.mockConstruction(
SFTPv3Client.class,
(mock, context) -> {
final SFTPv3FileAttributes attributes = new SFTPv3FileAttributes();
attributes.size = 1073741824L;
Mockito.when(mock.stat(filePath)).thenReturn(attributes);
}
)
) {
Mockito.doReturn(sshConnection).when(sshClient).getSshConnection();
Mockito.doReturn(true).when(sshConnection).isAuthenticationComplete();

Assertions.assertEquals(1073741824L, sshClient.fileSize(filePath));
}
}
}
Loading