들어가면서
- SFTP 를 이용해서 파일 업로드 하는 방법을 알아보겠습니다.
- 여러가지 방법이 있지만, 저는 apache 에 있는 VFS 를 이용해서 진행을 하겠습니다.
의존성
- 기본적으로 vfs2 의존성을 넣어주면 됩니다.
- 하지만, jsch 를 넣어주지 않으면 sftp 로 전송을 할 수 없습니다.
compile('org.apache.commons:commons-vfs2:2.7.0') compile('com.jcraft:jsch:0.1.55')
업로드 코드
public void upload(File file, String targetDir, String targetFilename, ConnectionConfig connectionConfig) {
try (StandardFileSystemManager manager = new StandardFileSystemManager()) {
manager.init();
FileObject localFile = manager.toFileObject(file);
String connectionString = createConnectionString(connectionConfig.getHost(), connectionConfig.getUsername(), connectionConfig.getPassword(), targetDir + "/" + targetFilename);
FileObject remoteFile = manager.resolveFile(connectionString, createDefaultOptions());
remoteFile.copyFrom(localFile, Selectors.SELECT_SELF);
System.out.println("File upload success");
} catch (Exception e) {
log.error("파일업로드에 실패했습니다.", e);
}
}
public static String createConnectionString(String hostName, String username, String password, String remoteFilePath) {
return "sftp://" + username + ":" + password + "@" + hostName + remoteFilePath;
}
public static FileSystemOptions createDefaultOptions() throws FileSystemException {
FileSystemOptions opts = new FileSystemOptions();
// SSH Key checking
SftpFileSystemConfigBuilder.getInstance().setStrictHostKeyChecking(opts, "no");
// Root directory set to user home
SftpFileSystemConfigBuilder.getInstance().setUserDirIsRoot(opts, false);
// Timeout is count by Milliseconds
SftpFileSystemConfigBuilder.getInstance().setSessionTimeoutMillis(opts, 10000);
return opts;
}
public class ConnectionConfig {
private String hostname;
private int port;
private String username;
private String password;
@Builder
public ConnectionConfig(String hostname, int port, String username, String password) {
this.hostname = hostname;
this.port = port;
this.username = username;
this.password = password;
}
public String getHost() {
return String.format("%s:%s", hostname, port);
}
}
문제 해결
jcsh 의존성이 없을 때
- vfs2 패키지에 들어가서 보면
providers.xml
파일이 있습니다. -
여기서 sftp schema 설정이 되어있는데,
com.jcraft.jsch.JSch
를 필요로 합니다.<providers> <!-- 중략.. --> <provider class-name="org.apache.commons.vfs2.provider.sftp.SftpFileProvider"> <scheme name="sftp"/> <if-available class-name="javax.crypto.Cipher"/> <if-available class-name="com.jcraft.jsch.JSch"/> </provider> <!-- 중략.. --> </providers>
- 이 설정이 없다면
DefaultFileSystemManager#resolveFile
에서 에러가 발생이 됩니다. -
오류 StackTrace
org.apache.commons.vfs2.FileSystemException: Could not find file with URI "sftp://user:***@host/path/filename.csv" because it is a relative path, and no base URI was provided. at org.apache.commons.vfs2.FileSystemException.requireNonNull(FileSystemException.java:87) at org.apache.commons.vfs2.impl.DefaultFileSystemManager.resolveFile(DefaultFileSystemManager.java:728) at org.apache.commons.vfs2.impl.DefaultFileSystemManager.resolveFile(DefaultFileSystemManager.java:648)
비밀번호에 @
가 있으면 안된다.
-
HostFileNameParser#extractToPath
에 보면 전달 된 URI 를 파싱하고 있습다.// 중략 final String userInfo = extractUserInfo(name); final String userName; final String password; // 중략
-
extractUserInfo
소스코드를 보면 URI 에서@
를 찾아서 거기까지 잘라서 userInfo 로 리턴을 합니다.-
앞에서 순차검색을 해서
@
까지 잘라서, userInfo 로 자르기 때문에 잘못된 파싱이 되어서 에러가 발생이 됩니다.final int maxlen = name.length(); for (int pos = 0; pos < maxlen; pos++) { final char ch = name.charAt(pos); if (ch == '@') { // Found the end of the user info final String userInfo = name.substring(0, pos); name.delete(0, pos + 1); return userInfo; } if (ch == '/' || ch == '?') { // Not allowed in user info break; } } return null;
-
- 오류 StackTrace
org.apache.commons.vfs2.FileSystemException: Invalid absolute URI "sftp://user:***@host/path/filename.csv". at org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider.findFile(AbstractOriginatingFileProvider.java:52) at org.apache.commons.vfs2.impl.DefaultFileSystemManager.resolveFile(DefaultFileSystemManager.java:711) at org.apache.commons.vfs2.impl.DefaultFileSystemManager.resolveFile(DefaultFileSystemManager.java:648)
비밀번호에 #
이 있으면 안된다.
- 위에
@
문제를 통과를 하고나면,UriParser#checkUriEncoding
decoding 을 하게 됩니다. - 여기서
#
문자열이 있다면 HEX값으로 생각을 하고 decoding 을 하게 됩니다. - 그래서 저는 password 를 encoding 해서 설정했습니다.
public static String createConnectionString(String hostName, String username, String password, String remoteFilePath) throws UnsupportedEncodingException { return "sftp://" + username + ":" + URLEncoder.encode(password, StandardCharsets.UTF_8.name()) + "@" + hostName + remoteFilePath; }
- 위 예제 코드에 있는
createConnectionString
를 encoding 하는것으로 변경하면 더이상 에러는 발생하지 않을겁니다. - 바로 위에 있는
@
문제도 encoding 을 하니까 해결이 되네요 - 오류 StackTrace
org.apache.commons.vfs2.FileSystemException: Invalid absolute URI "sftp://user:***@host/path/filename.csv". at org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider.findFile(AbstractOriginatingFileProvider.java:52) at org.apache.commons.vfs2.impl.DefaultFileSystemManager.resolveFile(DefaultFileSystemManager.java:711) at org.apache.commons.vfs2.impl.DefaultFileSystemManager.resolveFile(DefaultFileSystemManager.java:648)
참고 사이트
https://commons.apache.org/proper/commons-vfs/
https://m.blog.naver.com/PostView.nhn?blogId=racoon_z&logNo=220915890265&proxyReferer=https:%2F%2Fwww.google.com%2F
https://www.baeldung.com/java-file-sftp