Spring Boot + Spring Integration でいろいろ試してみる ( その29 )( Docker Compose でサーバを構築する、FTP+SFTPサーバ編 )
概要
記事一覧はこちらです。
Spring Integration のアプリケーションで使用するサーバを Docker Compose で構築します。
- FTPサーバ+SFTPサーバを構築します。
- FTPサーバの Dockerイメージは stilliard/pure-ftpd を使用します。
- SFTPサーバの Dockerイメージは atmoz/sftp を使用します。
- プロジェクトは Spring Boot + Spring Integration でいろいろ試してみる ( その25 )( Docker Compose でサーバを構築する、SMTP+POP3サーバ編 ) で作成した ksbysample-eipapp-dockerserver を使います。
- 今回はファイルのアップロード・ダウンロードを行うので、アプリケーションを実行するディレクトリ構成を以下のようにします。ftp ディレクトリが FTPサーバとの、sftp ディレクトリが SFTP サーバとのアップロード・ダウンロードを行うためのディレクトリです。
D:\eipapp\ksbysample-eipapp-dockerserver ├ ftp │ ├ download │ ├ upload │ └ uploading └ sftp ├ download ├ upload └ uploading
参照したサイト・書籍
stilliard/pure-ftpd(Docker Hub のページ)
https://hub.docker.com/r/stilliard/pure-ftpd/stilliard/docker-pure-ftpd(GitHub のページ)
https://github.com/stilliard/docker-pure-ftpdatmoz/sftp(Docker Hub のページ)
https://hub.docker.com/r/atmoz/sftp/atmoz/sftp(GitHub のページ)
https://github.com/atmoz/sftp
目次
- FTP サーバを構築する
- SFTP サーバを構築する
- FTP でアップロードするサンプルを作成する
- FTP でダウンロードするサンプルを作成する
- SFTP でアップロード・ダウンロードするサンプルを作成する
手順
FTP サーバを構築する
docker-compose.yml を変更する
docker-compose.yml の以下の点を変更します。
version: '3' services: .......... # stilliard/pure-ftpd # https://hub.docker.com/r/stilliard/pure-ftpd/ # # 起動した pure-ftpd のコンテナ(ftp-server) にアクセスする場合には以下のコマンドを実行する # docker exec -it ftp-server /bin/bash # ftp-server: image: stilliard/pure-ftpd:latest container_name: ftp-server ports: - "21:21" - "30000-30009:30000-30009" environment: - PUBLICHOST=localhost - FTP_USER_NAME=test - FTP_USER_PASS=12345678 - FTP_USER_HOME=/home/ftpusers/test restart: always
services
にftp-server
を追加します。- FTPユーザは環境変数 FTP_USER_NAME, FTP_USER_PASS, FTP_USER_HOME で設定します。
- この方法だと1ユーザしか設定できません。複数ユーザを作成したい場合には https://download.pureftpd.org/pure-ftpd/doc/README.Virtual-Users を参考に作成します。
- image は
stilliard/pure-ftpd:hardened
ではなくstilliard/pure-ftpd:latest
を指定します。stilliard/pure-ftpd:hardened
だと Spring Integration でファイルをアップロードできません(Spring Integration はファイルアップロード時にファイル名の最後に .writing という文字列を付けてアップロードし、完了後にリネームで取り除くのですが、stilliard/pure-ftpd:hardened
だとリネームができませんでした)。
サーバを起動する
コマンドプロンプトを起動し docker-compose.yml のあるディレクトリへ移動して docker-compose up -d
コマンドを実行して起動します。
IntelliJ IDEA の docker plugin を見ると ftp-server コンテナが起動していることが確認できます。
動作確認
WinSCP で接続してみます。「ログイン」ダイアログでホスト名、ユーザ名、パスワードを入力して「ログイン」ボタンをクリックします。
無事ログインできました。ファイルのアップロード、ダウンロードも問題なく出来ました。
一旦 docker-compose down
コマンドを実行してコンテナを停止・削除します。
SFTP サーバを構築する
users.conf を作成する
プロジェクトの直下に docker/sftp-server/config ディレクトリを作成します。
docker/sftp-server/config の下に users.conf を新規作成し、以下の内容を記述します。
user01:pass01:::upload,download user02:pass02:::upload,download user03:pass03:::upload,download
- 改行コードは LF にします(CRLF だとログインできません)。
- ログインした直後のルートディレクトリにはファイルをアップロードできません(Permission denied が表示されます、ただしダウンロードは可能)。アップロードしたい場合には、アップロード先のディレクトリ名を users.conf の一番右の場所に記述します(複数ディレクトリを作成したい場合にはカンマで区切ります)。
以下のディレクトリ構成になります。
docker-compose.yml を変更する
docker-compose.yml の以下の点を変更します。
version: '3' services: .......... # atmoz/sftp # https://hub.docker.com/r/atmoz/sftp/ # # 起動した sftp のコンテナ(sftp-server) にアクセスする場合には以下のコマンドを実行する # docker exec -it sftp-server /bin/bash # sftp-server: image: atmoz/sftp container_name: sftp-server ports: - "22:22" volumes: - ./docker/sftp-server/config/users.conf:/etc/sftp/users.conf:ro
サーバを起動する
docker-compose up -d
コマンドを実行して起動します。
IntelliJ IDEA の docker plugin を見ると sftp-server コンテナが起動していることが確認できます。
動作確認
WinSCP で接続してみます。「ログイン」ダイアログでホスト名、ユーザ名、パスワードを入力して「ログイン」ボタンをクリックします。
「警告」ダイアログが表示されますので、「更新」ボタンをクリックします。
無事ログインできました。download, upload ディレクトリへファイルのアップロード、ダウンロードも問題なく出来ました。
FTP でアップロードするサンプルを作成する
最初にアプリケーションを起動した時にメールのサンプルが実行されないよう src/main/java/ksbysample/eipapp/dockerserver/flow/MailFlowConfig.java の @Configuration
アノテーションをコメントアウトします。
// このサンプルを実行したい場合には、@Configuration のコメントアウトを外すこと //@Configuration public class MailFlowConfig {
build.gradle の以下の点を変更します。
dependencies { implementation('org.springframework.boot:spring-boot-starter-integration') implementation("org.springframework.boot:spring-boot-starter-mail") implementation('org.springframework.integration:spring-integration-mail') implementation('org.springframework.integration:spring-integration-file') implementation('org.springframework.integration:spring-integration-ftp') implementation("org.apache.commons:commons-lang3") testImplementation('org.springframework.boot:spring-boot-starter-test') }
- 以下の行を追加します。
implementation('org.springframework.integration:spring-integration-file')
implementation('org.springframework.integration:spring-integration-ftp')
build.gradle を変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
src/main/java/ksbysample/eipapp/dockerserver/flow の下に FtpFlowConfig.java を新規作成し、以下の内容を記述します。
package ksbysample.eipapp.dockerserver.flow; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPFile; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlows; import org.springframework.integration.dsl.Pollers; import org.springframework.integration.file.FileReadingMessageSource; import org.springframework.integration.file.filters.AcceptAllFileListFilter; import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler; import org.springframework.integration.file.remote.session.CachingSessionFactory; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.ftp.session.DefaultFtpSessionFactory; import org.springframework.integration.handler.LoggingHandler; import org.springframework.messaging.support.GenericMessage; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; @Configuration public class FtpFlowConfig { private static final String FTP_SERVER = "localhost"; private static final int FTP_PORT = 21; private static final String FTP_USER = "test"; private static final String FTP_PASSWORD = "12345678"; private static final String FTP_REMOTE_DIR = "/"; private static final String FTP_LOCAL_ROOT_DIR = "D:/eipapp/ksbysample-eipapp-dockerserver/ftp"; private static final String FTP_LOCAL_UPLOAD_DIR = FTP_LOCAL_ROOT_DIR + "/upload"; private static final String FTP_LOCAL_UPLOADING_DIR = FTP_LOCAL_ROOT_DIR + "/uploading"; @Bean public SessionFactory<FTPFile> ftpSessionFactory() { DefaultFtpSessionFactory factory = new DefaultFtpSessionFactory(); factory.setHost(FTP_SERVER); factory.setPort(FTP_PORT); factory.setUsername(FTP_USER); factory.setPassword(FTP_PASSWORD); factory.setClientMode(FTPClient.PASSIVE_LOCAL_DATA_CONNECTION_MODE); return new CachingSessionFactory<>(factory); } /**************************************** * FTPアップロード処理のサンプル * ****************************************/ @Bean public FileReadingMessageSource ftpUploadFileMessageSource() { FileReadingMessageSource source = new FileReadingMessageSource(); source.setDirectory(new File(FTP_LOCAL_UPLOAD_DIR)); source.setFilter(new AcceptAllFileListFilter<>()); return source; } @Bean public FileTransferringMessageHandler<FTPFile> ftpFileTransferringMessageHandler() { FileTransferringMessageHandler<FTPFile> handler = new FileTransferringMessageHandler<>(ftpSessionFactory()); handler.setRemoteDirectoryExpression(new LiteralExpression(FTP_REMOTE_DIR)); return handler; } @Bean public IntegrationFlow ftpUploadFlow() { return IntegrationFlows.from( // 200ミリ秒毎に ftp ディレクトリを監視し、ファイルがあれば処理を進める ftpUploadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(200))) // ファイルを uploading ディレクトリへ移動する .<File>handle((p, h) -> { try { Path movedFilePath = Files.move(p.toPath(), Paths.get(FTP_LOCAL_UPLOADING_DIR, p.getName()) , StandardCopyOption.REPLACE_EXISTING); return new GenericMessage<>(movedFilePath.toFile(), h); } catch (IOException e) { throw new RuntimeException(e); } }) // FTPサーバにファイルをアップロードする .wireTap(f -> f.handle(ftpFileTransferringMessageHandler())) .log(LoggingHandler.Level.WARN) // アップロードしたファイルを削除する .<File>handle((p, h) -> { p.delete(); return null; }) .get(); } }
動作確認してみます。最初に docker-compose up -d
で FTP サーバを起動し、WinSCP で何もアップロードされていないことを確認します。
アプリケーションを起動後、D:\eipapp\ksbysample-eipapp-dockerserver\ftp\upload の下にファイルを1つ配置します。ファイルをアップロードしたことを示すログが出力されることが確認できます。
WinSCP で見ると配置したファイルがアップロードされていることが確認できます。
アップロードされたファイルを削除し、アプリケーションを停止します。
FTP でダウンロードするサンプルを作成する
src/main/java/ksbysample/eipapp/dockerserver/flow/FtpFlowConfig.java に FTP ダウンロード処理を追加します。
@Configuration public class FtpFlowConfig { .......... private static final String FTP_LOCAL_DOWNLOAD_DIR = FTP_LOCAL_ROOT_DIR + "/download"; .......... /**************************************** * FTPダウンロード処理のサンプル * ****************************************/ @Bean public FtpInboundFileSynchronizer ftpInboundFileSynchronizer() { FtpInboundFileSynchronizer synchronizer = new FtpInboundFileSynchronizer(ftpSessionFactory()); synchronizer.setRemoteDirectory(FTP_REMOTE_DIR); synchronizer.setFilter(new AcceptAllFileListFilter<>()); synchronizer.setPreserveTimestamp(true); synchronizer.setDeleteRemoteFiles(true); return synchronizer; } @Bean public FtpInboundFileSynchronizingMessageSource ftpDownloadFileMessageSource() { FtpInboundFileSynchronizingMessageSource messageSource = new FtpInboundFileSynchronizingMessageSource(ftpInboundFileSynchronizer()); messageSource.setLocalDirectory(new File(FTP_LOCAL_DOWNLOAD_DIR)); messageSource.setLocalFilter(new AcceptAllFileListFilter<>()); messageSource.setMaxFetchSize(1); return messageSource; } @Bean public IntegrationFlow ftpDownloadFlow() { return IntegrationFlows.from( // 1秒毎に FTPサーバを監視し、ファイルがあれば download ディレクトリにダウンロードする ftpDownloadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(1000))) .log(LoggingHandler.Level.ERROR) // ファイルを upload ディレクトリへ移動する .<File>handle((p, h) -> { try { Files.move(p.toPath(), Paths.get(FTP_LOCAL_UPLOAD_DIR, p.getName()) , StandardCopyOption.REPLACE_EXISTING); return null; } catch (IOException e) { throw new RuntimeException(e); } }) .get(); } }
動作確認します。アプリケーションを起動後、D:\eipapp\ksbysample-eipapp-dockerserver\ftp\upload の下にファイルを1つ配置します。ファイルのアップロード・ダウンロードが繰り返されていることを示すログが出力されます。
アプリケーションを停止し、docker-compose down
で FTP サーバも停止します。
アプリケーションを起動した時に FTP のサンプルが実行されないよう src/main/java/ksbysample/eipapp/dockerserver/flow/FtpFlowConfig.java の @Configuration アノテーションをコメントアウトします。
// このサンプルを実行したい場合には、@Configuration のコメントアウトを外すこと //@Configuration public class FtpFlowConfig {
SFTP でアップロード・ダウンロードするサンプルを作成する
SFTP のサンプルは FTP とほぼ同じ書き方になるので、アップロード・ダウンロードのサンプルを一気に作成します。
build.gradle の以下の点を変更します。
dependencies { implementation('org.springframework.boot:spring-boot-starter-integration') implementation("org.springframework.boot:spring-boot-starter-mail") implementation('org.springframework.integration:spring-integration-mail') implementation('org.springframework.integration:spring-integration-file') implementation('org.springframework.integration:spring-integration-ftp') implementation('org.springframework.integration:spring-integration-sftp') implementation("org.apache.commons:commons-lang3") testImplementation('org.springframework.boot:spring-boot-starter-test') }
implementation('org.springframework.integration:spring-integration-sftp')
を追加します。
build.gradle を変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
src/main/java/ksbysample/eipapp/dockerserver/flow の下に SftpFlowConfig.java を新規作成し、以下の内容を記述します。
package ksbysample.eipapp.dockerserver.flow; import com.jcraft.jsch.ChannelSftp; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlows; import org.springframework.integration.dsl.Pollers; import org.springframework.integration.file.FileReadingMessageSource; import org.springframework.integration.file.filters.AcceptAllFileListFilter; import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler; import org.springframework.integration.file.remote.session.CachingSessionFactory; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.handler.LoggingHandler; import org.springframework.integration.sftp.inbound.SftpInboundFileSynchronizer; import org.springframework.integration.sftp.inbound.SftpInboundFileSynchronizingMessageSource; import org.springframework.integration.sftp.session.DefaultSftpSessionFactory; import org.springframework.messaging.support.GenericMessage; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; @Configuration public class SftpFlowConfig { private static final String SFTP_SERVER = "localhost"; private static final int SFTP_PORT = 22; private static final String SFTP_USER = "user01"; private static final String SFTP_PASSWORD = "pass01"; private static final String SFTP_REMOTE_DIR = "/upload"; private static final String SFTP_LOCAL_ROOT_DIR = "D:/eipapp/ksbysample-eipapp-dockerserver/sftp"; private static final String SFTP_LOCAL_UPLOAD_DIR = SFTP_LOCAL_ROOT_DIR + "/upload"; private static final String SFTP_LOCAL_UPLOADING_DIR = SFTP_LOCAL_ROOT_DIR + "/uploading"; private static final String SFTP_LOCAL_DOWNLOAD_DIR = SFTP_LOCAL_ROOT_DIR + "/download"; @Bean public SessionFactory<ChannelSftp.LsEntry> sftpSessionFactory() { DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(); factory.setHost(SFTP_SERVER); factory.setPort(SFTP_PORT); factory.setUser(SFTP_USER); factory.setPassword(SFTP_PASSWORD); factory.setAllowUnknownKeys(true); return new CachingSessionFactory<>(factory); } /**************************************** * SFTPアップロード処理のサンプル * ****************************************/ @Bean public FileReadingMessageSource sftpUploadFileMessageSource() { FileReadingMessageSource source = new FileReadingMessageSource(); source.setDirectory(new File(SFTP_LOCAL_UPLOAD_DIR)); source.setFilter(new AcceptAllFileListFilter<>()); return source; } @Bean public FileTransferringMessageHandler<ChannelSftp.LsEntry> sftpFileTransferringMessageHandler() { FileTransferringMessageHandler<ChannelSftp.LsEntry> handler = new FileTransferringMessageHandler<>(sftpSessionFactory()); handler.setRemoteDirectoryExpression(new LiteralExpression(SFTP_REMOTE_DIR)); return handler; } @Bean public IntegrationFlow sftpUploadFlow() { return IntegrationFlows.from( // 200ミリ秒毎に upload ディレクトリを監視し、ファイルがあれば処理を進める sftpUploadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(200))) // ファイルを uploading ディレクトリへ移動する .<File>handle((p, h) -> { try { Path movedFilePath = Files.move(p.toPath(), Paths.get(SFTP_LOCAL_UPLOADING_DIR, p.getName()) , StandardCopyOption.REPLACE_EXISTING); return new GenericMessage<>(movedFilePath.toFile(), h); } catch (IOException e) { throw new RuntimeException(e); } }) // SFTPサーバにファイルをアップロードする .wireTap(f -> f.handle(sftpFileTransferringMessageHandler())) .log(LoggingHandler.Level.WARN) // アップロードしたファイルを削除する .<File>handle((p, h) -> { p.delete(); return null; }) .get(); } /**************************************** * SFTPダウンロード処理のサンプル * ****************************************/ @Bean public SftpInboundFileSynchronizer sftpInboundFileSynchronizer() { SftpInboundFileSynchronizer synchronizer = new SftpInboundFileSynchronizer(sftpSessionFactory()); synchronizer.setRemoteDirectory(SFTP_REMOTE_DIR); synchronizer.setFilter(new AcceptAllFileListFilter<>()); synchronizer.setPreserveTimestamp(true); synchronizer.setDeleteRemoteFiles(true); return synchronizer; } @Bean public SftpInboundFileSynchronizingMessageSource sftpDownloadFileMessageSource() { SftpInboundFileSynchronizingMessageSource messageSource = new SftpInboundFileSynchronizingMessageSource(sftpInboundFileSynchronizer()); messageSource.setLocalDirectory(new File(SFTP_LOCAL_DOWNLOAD_DIR)); messageSource.setLocalFilter(new AcceptAllFileListFilter<>()); messageSource.setMaxFetchSize(1); return messageSource; } @Bean public IntegrationFlow sftpDownloadFlow() { return IntegrationFlows.from( // 1秒毎に SFTPサーバを監視し、ファイルがあれば download ディレクトリにダウンロードする sftpDownloadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(1000))) .log(LoggingHandler.Level.ERROR) // ファイルを upload ディレクトリへ移動する .<File>handle((p, h) -> { try { Files.move(p.toPath(), Paths.get(SFTP_LOCAL_UPLOAD_DIR, p.getName()) , StandardCopyOption.REPLACE_EXISTING); return null; } catch (IOException e) { throw new RuntimeException(e); } }) .get(); } }
動作確認します。最初に docker-compose up -d
で SFTP サーバを起動します。
アプリケーションを起動後、D:\eipapp\ksbysample-eipapp-dockerserver\sftp\upload の下にファイルを1つ配置します。ファイルのアップロード・ダウンロードが繰り返されていることを示すログが出力されることが確認できます。
アプリケーションを停止し、docker-compose down
で SFTP サーバも停止します。
アプリケーションを起動した時に FTP のサンプルが実行されないよう src/main/java/ksbysample/eipapp/dockerserver/flow/SftpFlowConfig.java の @Configuration アノテーションをコメントアウトします。
// このサンプルを実行したい場合には、@Configuration のコメントアウトを外すこと //@Configuration public class SftpFlowConfig {
履歴
2018/09/24
初版発行。