かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot + Spring Integration でいろいろ試してみる ( その29 )( Docker Compose でサーバを構築する、FTP+SFTPサーバ編 )

概要

記事一覧はこちらです。

Spring Integration のアプリケーションで使用するサーバを Docker Compose で構築します。

D:\eipapp\ksbysample-eipapp-dockerserver
├ ftp
│ ├ download
│ ├ upload
│ └ uploading
└ sftp
   ├ download
   ├ upload
   └ uploading

参照したサイト・書籍

目次

  1. FTP サーバを構築する
    1. docker-compose.yml を変更する
    2. サーバを起動する
    3. 動作確認
  2. SFTP サーバを構築する
    1. users.conf を作成する
    2. docker-compose.yml を変更する
    3. サーバを起動する
    4. 動作確認
  3. FTP でアップロードするサンプルを作成する
  4. FTP でダウンロードするサンプルを作成する
  5. 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
  • servicesftp-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 コマンドを実行して起動します。

f:id:ksby:20180923143047p:plain

IntelliJ IDEA の docker plugin を見ると ftp-server コンテナが起動していることが確認できます。

f:id:ksby:20180923143153p:plain

動作確認

WinSCP で接続してみます。「ログイン」ダイアログでホスト名、ユーザ名、パスワードを入力して「ログイン」ボタンをクリックします。

f:id:ksby:20180923021517p:plain

無事ログインできました。ファイルのアップロード、ダウンロードも問題なく出来ました。

f:id:ksby:20180923021858p:plain

一旦 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 の一番右の場所に記述します(複数ディレクトリを作成したい場合にはカンマで区切ります)。

以下のディレクトリ構成になります。

f:id:ksby:20180923051753p:plain

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 コマンドを実行して起動します。

f:id:ksby:20180923111811p:plain

IntelliJ IDEA の docker plugin を見ると sftp-server コンテナが起動していることが確認できます。

f:id:ksby:20180923144007p:plain

動作確認

WinSCP で接続してみます。「ログイン」ダイアログでホスト名、ユーザ名、パスワードを入力して「ログイン」ボタンをクリックします。

f:id:ksby:20180923112237p:plain

「警告」ダイアログが表示されますので、「更新」ボタンをクリックします。

f:id:ksby:20180923112547p:plain

無事ログインできました。download, upload ディレクトリへファイルのアップロード、ダウンロードも問題なく出来ました。

f:id:ksby:20180923112738p:plain

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 -dFTP サーバを起動し、WinSCP で何もアップロードされていないことを確認します。

f:id:ksby:20180923150702p:plain

アプリケーションを起動後、D:\eipapp\ksbysample-eipapp-dockerserver\ftp\upload の下にファイルを1つ配置します。ファイルをアップロードしたことを示すログが出力されることが確認できます。

f:id:ksby:20180923150938p:plain

WinSCP で見ると配置したファイルがアップロードされていることが確認できます。

f:id:ksby:20180923151032p:plain

アップロードされたファイルを削除し、アプリケーションを停止します。

FTP でダウンロードするサンプルを作成する

src/main/java/ksbysample/eipapp/dockerserver/flow/FtpFlowConfig.javaFTP ダウンロード処理を追加します。

@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つ配置します。ファイルのアップロード・ダウンロードが繰り返されていることを示すログが出力されます。

f:id:ksby:20180923230000p:plain

アプリケーションを停止し、docker-compose downFTP サーバも停止します。

アプリケーションを起動した時に 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つ配置します。ファイルのアップロード・ダウンロードが繰り返されていることを示すログが出力されることが確認できます。

f:id:ksby:20180924212652p:plain f:id:ksby:20180924212822p:plain

アプリケーションを停止し、docker-compose down で SFTP サーバも停止します。

アプリケーションを起動した時に FTP のサンプルが実行されないよう src/main/java/ksbysample/eipapp/dockerserver/flow/SftpFlowConfig.java の @Configuration アノテーションコメントアウトします。

// このサンプルを実行したい場合には、@Configuration のコメントアウトを外すこと
//@Configuration
public class SftpFlowConfig {

履歴

2018/09/24
初版発行。