SpringBoot + SFTP 實(shí)現(xiàn)文件上傳與下載實(shí)戰(zhàn)

背景

近期在工作中需要實(shí)現(xiàn)文件的上傳與下載,一開(kāi)始打算使用一些高級(jí)的文件系統(tǒng),比如:FastDFS,GlusterFS,CephFS,這些高級(jí)厲害的文件存儲(chǔ)系統(tǒng),當(dāng)然博主也花了兩周的時(shí)間把這三個(gè)FS都玩了一遍。個(gè)人認(rèn)為FastDFS使用以及部署最簡(jiǎn)單,比較適合存儲(chǔ)圖片以及中小型文件(<500M),畢竟是國(guó)產(chǎn)框架(點(diǎn)贊);而GlusterFS和CephFS,GlusterFS部署和Java對(duì)接起來(lái)較為簡(jiǎn)單,CephFS部署很費(fèi)勁,對(duì)Java使用不太友好(不太方便)。當(dāng)然很大原因是博主技術(shù)不夠,玩不過(guò)來(lái)。在使用這些框架之后,Leader感覺(jué)公司目前的技術(shù)儲(chǔ)備還不夠成熟,最終使用常用的SFTP實(shí)現(xiàn)文件上傳和下載。背景介紹就到這里,接下來(lái)實(shí)戰(zhàn)吧!

SFTP介紹

  • SFTP是Secure File Transfer Protocol的縮寫(xiě),安全文件傳送協(xié)議??梢詾閭鬏斘募峁┮环N安全的加密方法,語(yǔ)法幾乎和FTP一致。
  • 相比于FTP,SFTP更安全,但更安全帶來(lái)副作用就是的效率比FTP要低些。
  • SFTP是SSH的一部分,內(nèi)部是采用SSH連接,所以在以下代碼中進(jìn)行文件的操作都會(huì)先cd到SFTP存放文件的根路徑下。
  • Reference:SFTP與FTP比較淺談SFTP與FTP。

實(shí)戰(zhàn)

1. 相關(guān)依賴(基于SpringBoot)
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>com.jcraft</groupId>
        <artifactId>jsch</artifactId>
        <version>0.1.54</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
</dependencies>
2. 相關(guān)配置
#============================================================================
# SFTP Client Setting
#============================================================================
# 協(xié)議
sftp.client.protocol=sftp
# ip地址
sftp.client.host=127.0.0.1
# 端口
sftp.client.port=22
# 用戶名
sftp.client.username=sftp
# 密碼
sftp.client.password=sftp
# 根路徑
sftp.client.root=/home/sftp/
# 密鑰文件路徑
sftp.client.privateKey=
# 密鑰的密碼
sftp.client.passphrase=
# 
sftp.client.sessionStrictHostKeyChecking=no
# session連接超時(shí)時(shí)間
sftp.client.sessionConnectTimeout=15000
# channel連接超時(shí)時(shí)間
sftp.client.channelConnectedTimeout=15000
  • 這里暫時(shí)沒(méi)有使用到使用加密密鑰的方式登陸,所以暫不填寫(xiě)
3. 將application.properties中配置轉(zhuǎn)為一個(gè)Bean
@Getter
@Setter
@Component
@ConfigurationProperties(ignoreUnknownFields = false, prefix = "sftp.client")
public class SftpProperties {
    private String host;

    private Integer port;

    private String protocol;

    private String username;

    private String password;

    private String root;

    private String privateKey;

    private String passphrase;

    private String sessionStrictHostKeyChecking;

    private Integer sessionConnectTimeout;

    private Integer channelConnectedTimeout;
}
4. 將上傳下載文件封裝成Service
  • FileSystemService
/**
 * @author jason.tang
 * @create 2019-03-07 13:33
 * @description
 */
public interface FileSystemService {

    boolean uploadFile(String targetPath, InputStream inputStream) throws Exception;

    boolean uploadFile(String targetPath, File file) throws Exception;

    File downloadFile(String targetPath) throws Exception;

    boolean deleteFile(String targetPath) throws Exception;
}
  • 實(shí)現(xiàn)類:FileSystemServiceImpl(此處省略相關(guān)上傳下載代碼)
/**
 * @author jason.tang
 * @create 2019-03-07 13:33
 * @description
 */
@Slf4j
@Service("fileSystemService")
public class FileSystemServiceImpl implements FileSystemService {

    @Autowired
    private SftpProperties config;

    // 設(shè)置第一次登陸的時(shí)候提示,可選值:(ask | yes | no)
    private static final String SESSION_CONFIG_STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking";

    /**
     * 創(chuàng)建SFTP連接
     * @return
     * @throws Exception
     */
    private ChannelSftp createSftp() throws Exception {
        JSch jsch = new JSch();
        log.info("Try to connect sftp[" + config.getUsername() + "@" + config.getHost() + "], use password[" + config.getPassword() + "]");

        Session session = createSession(jsch, config.getHost(), config.getUsername(), config.getPort());
        session.setPassword(config.getPassword());
        session.connect(config.getSessionConnectTimeout());

        log.info("Session connected to {}.", config.getHost());

        Channel channel = session.openChannel(config.getProtocol());
        channel.connect(config.getChannelConnectedTimeout());

        log.info("Channel created to {}.", config.getHost());

        return (ChannelSftp) channel;
    }

    /**
     * 加密秘鑰方式登陸
     * @return
     */
    private ChannelSftp connectByKey() throws Exception {
        JSch jsch = new JSch();

        // 設(shè)置密鑰和密碼 ,支持密鑰的方式登陸
        if (StringUtils.isNotBlank(config.getPrivateKey())) {
            if (StringUtils.isNotBlank(config.getPassphrase())) {
                // 設(shè)置帶口令的密鑰
                jsch.addIdentity(config.getPrivateKey(), config.getPassphrase());
            } else {
                // 設(shè)置不帶口令的密鑰
                jsch.addIdentity(config.getPrivateKey());
            }
        }
        log.info("Try to connect sftp[" + config.getUsername() + "@" + config.getHost() + "], use private key[" + config.getPrivateKey()
                + "] with passphrase[" + config.getPassphrase() + "]");

        Session session = createSession(jsch, config.getHost(), config.getUsername(), config.getPort());
        // 設(shè)置登陸超時(shí)時(shí)間
        session.connect(config.getSessionConnectTimeout());
        log.info("Session connected to " + config.getHost() + ".");

        // 創(chuàng)建sftp通信通道
        Channel channel = session.openChannel(config.getProtocol());
        channel.connect(config.getChannelConnectedTimeout());
        log.info("Channel created to " + config.getHost() + ".");
        return (ChannelSftp) channel;
    }

    /**
     * 創(chuàng)建session
     * @param jsch
     * @param host
     * @param username
     * @param port
     * @return
     * @throws Exception
     */
    private Session createSession(JSch jsch, String host, String username, Integer port) throws Exception {
        Session session = null;

        if (port <= 0) {
            session = jsch.getSession(username, host);
        } else {
            session = jsch.getSession(username, host, port);
        }

        if (session == null) {
            throw new Exception(host + " session is null");
        }

        session.setConfig(SESSION_CONFIG_STRICT_HOST_KEY_CHECKING, config.getSessionStrictHostKeyChecking());
        return session;
    }

    /**
     * 關(guān)閉連接
     * @param sftp
     */
    private void disconnect(ChannelSftp sftp) {
        try {
            if (sftp != null) {
                if (sftp.isConnected()) {
                    sftp.disconnect();
                } else if (sftp.isClosed()) {
                    log.info("sftp is closed already");
                }
                if (null != sftp.getSession()) {
                    sftp.getSession().disconnect();
                }
            }
        } catch (JSchException e) {
            e.printStackTrace();
        }
    }
}
5. 上傳文件
  • 5.1 將inputStream上傳到指定路徑下(單級(jí)或多級(jí)目錄)
@Override
public boolean uploadFile(String targetPath, InputStream inputStream) throws Exception {
    ChannelSftp sftp = this.createSftp();
    try {
        sftp.cd(config.getRoot());
        log.info("Change path to {}", config.getRoot());

        int index = targetPath.lastIndexOf("/");
        String fileDir = targetPath.substring(0, index);
        String fileName = targetPath.substring(index + 1);
        boolean dirs = this.createDirs(fileDir, sftp);
        if (!dirs) {
            log.error("Remote path error. path:{}", targetPath);
            throw new Exception("Upload File failure");
        }
        sftp.put(inputStream, fileName);
        return true;
    } catch (Exception e) {
        log.error("Upload file failure. TargetPath: {}", targetPath, e);
        throw new Exception("Upload File failure");
    } finally {
        this.disconnect(sftp);
    }
}
  • 5.2 創(chuàng)建多級(jí)目錄
private boolean createDirs(String dirPath, ChannelSftp sftp) {
    if (dirPath != null && !dirPath.isEmpty()
                && sftp != null) {
        String[] dirs = Arrays.stream(dirPath.split("/"))
               .filter(StringUtils::isNotBlank)
               .toArray(String[]::new);

        for (String dir : dirs) {
            try {
                sftp.cd(dir);
                log.info("Change directory {}", dir);
            } catch (Exception e) {
                try {
                    sftp.mkdir(dir);
                    log.info("Create directory {}", dir);
                } catch (SftpException e1) {
                     log.error("Create directory failure, directory:{}", dir, e1);
                     e1.printStackTrace();
                }
                try {
                    sftp.cd(dir);
                    log.info("Change directory {}", dir);
                } catch (SftpException e1) {
                    log.error("Change directory failure, directory:{}", dir, e1);
                    e1.printStackTrace();
                }
            }
        }
        return true;
    }
    return false;
}
  • 5.3 將文件上傳到指定目錄
@Override
public boolean uploadFile(String targetPath, File file) throws Exception {
    return this.uploadFile(targetPath, new FileInputStream(file));
}
6. 下載文件
@Override
public File downloadFile(String targetPath) throws Exception {
    ChannelSftp sftp = this.createSftp();
    OutputStream outputStream = null;
    try {
        sftp.cd(config.getRoot());
        log.info("Change path to {}", config.getRoot());

        File file = new File(targetPath.substring(targetPath.lastIndexOf("/") + 1));

        outputStream = new FileOutputStream(file);
        sftp.get(targetPath, outputStream);
        log.info("Download file success. TargetPath: {}", targetPath);
        return file;
    } catch (Exception e) {
        log.error("Download file failure. TargetPath: {}", targetPath, e);
         throw new Exception("Download File failure");
    } finally {
        if (outputStream != null) {
            outputStream.close();
        }
        this.disconnect(sftp);
    }
}
7. 刪除文件
/**
     * 刪除文件
     * @param targetPath
     * @return
     * @throws Exception
     */
@Override
public boolean deleteFile(String targetPath) throws Exception {
    ChannelSftp sftp = null;
    try {
        sftp = this.createSftp();
        sftp.cd(config.getRoot());
        sftp.rm(targetPath);
        return true;
    } catch (Exception e) {
        log.error("Delete file failure. TargetPath: {}", targetPath, e);
        throw new Exception("Delete File failure");
    } finally {
        this.disconnect(sftp);
    }
}
8. 最后
  • 具體測(cè)試請(qǐng)看源碼,這里不貼出相關(guān)測(cè)試,避免篇幅太長(zhǎng)。
  • 涉及到對(duì)文件的操作,一定記得將流關(guān)閉。
  • 在使用中比如下載文件,請(qǐng)將生成的文件在使用后刪除(file.delete()),避免在服務(wù)器中占據(jù)大量資源。
  • application.proerties中SFTP相關(guān)配置,請(qǐng)自行更換。如有不對(duì)之處,請(qǐng)指出,感謝閱讀!
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容