美文网首页Java 杂谈JavaSpringboot
SpringBoot + SFTP 实现文件上传与下载实战

SpringBoot + SFTP 实现文件上传与下载实战

作者: Java酸不酸 | 来源:发表于2019-03-19 15:43 被阅读14次

    背景

    近期在工作中需要实现文件的上传与下载,一开始打算使用一些高级的文件系统,比如:FastDFS,GlusterFS,CephFS,这些高级厉害的文件存储系统,当然博主也花了两周的时间把这三个FS都玩了一遍。个人认为FastDFS使用以及部署最简单,比较适合存储图片以及中小型文件(<500M),毕竟是国产框架(点赞);而GlusterFS和CephFS,GlusterFS部署和Java对接起来较为简单,CephFS部署很费劲,对Java使用不太友好(不太方便)。当然很大原因是博主技术不够,玩不过来。在使用这些框架之后,Leader感觉公司目前的技术储备还不够成熟,最终使用常用的SFTP实现文件上传和下载。背景介绍就到这里,接下来实战吧!

    SFTP介绍

    • SFTP是Secure File Transfer Protocol的缩写,安全文件传送协议。可以为传输文件提供一种安全的加密方法,语法几乎和FTP一致。
    • 相比于FTP,SFTP更安全,但更安全带来副作用就是的效率比FTP要低些。
    • SFTP是SSH的一部分,内部是采用SSH连接,所以在以下代码中进行文件的操作都会先cd到SFTP存放文件的根路径下。
    • Reference:SFTP与FTP比较浅谈SFTP与FTP

    实战

    1. 相关依赖(基于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. 相关配置
    #============================================================================
    # SFTP Client Setting
    #============================================================================
    # 协议
    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连接超时时间
    sftp.client.sessionConnectTimeout=15000
    # channel连接超时时间
    sftp.client.channelConnectedTimeout=15000
    
    • 这里暂时没有使用到使用加密密钥的方式登陆,所以暂不填写
    3. 将application.properties中配置转为一个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;
    }
    
    • 实现类:FileSystemServiceImpl(此处省略相关上传下载代码)
    /**
     * @author jason.tang
     * @create 2019-03-07 13:33
     * @description
     */
    @Slf4j
    @Service("fileSystemService")
    public class FileSystemServiceImpl implements FileSystemService {
    
        @Autowired
        private SftpProperties config;
    
        // 设置第一次登陆的时候提示,可选值:(ask | yes | no)
        private static final String SESSION_CONFIG_STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking";
    
        /**
         * 创建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();
    
            // 设置密钥和密码 ,支持密钥的方式登陆
            if (StringUtils.isNotBlank(config.getPrivateKey())) {
                if (StringUtils.isNotBlank(config.getPassphrase())) {
                    // 设置带口令的密钥
                    jsch.addIdentity(config.getPrivateKey(), config.getPassphrase());
                } else {
                    // 设置不带口令的密钥
                    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());
            // 设置登陆超时时间
            session.connect(config.getSessionConnectTimeout());
            log.info("Session connected to " + config.getHost() + ".");
    
            // 创建sftp通信通道
            Channel channel = session.openChannel(config.getProtocol());
            channel.connect(config.getChannelConnectedTimeout());
            log.info("Channel created to " + config.getHost() + ".");
            return (ChannelSftp) channel;
        }
    
        /**
         * 创建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;
        }
    
        /**
         * 关闭连接
         * @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上传到指定路径下(单级或多级目录)
    @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 创建多级目录
    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. 最后
    • 具体测试请看源码,这里不贴出相关测试,避免篇幅太长。
    • 涉及到对文件的操作,一定记得将流关闭。
    • 在使用中比如下载文件,请将生成的文件在使用后删除(file.delete()),避免在服务器中占据大量资源。
    • application.proerties中SFTP相关配置,请自行更换。如有不对之处,请指出,感谢阅读!

    相关文章

      网友评论

        本文标题:SpringBoot + SFTP 实现文件上传与下载实战

        本文链接:https://www.haomeiwen.com/subject/qajgmqtx.html