使用sftp操作文件并添加事务管理

时间:2024-07-24 23:05:38

  本文主要针对文件操作的事务管理,即写文件和删除文件并且能保证事务的一致性,可与数据库联合使用,比如需要在服务器存文件,相应的记录存放在数据库,那么数据库的记录和服务器的文件数一定是要一一对应的,该部分代码可以保证大多数情况下的文件部分的事务要求(特殊情况下面会说),和数据库保持一致的话需要自行添加数据库部分,比较简单。

  基本原理就是,添加文件时先在目录里添加一个临时的文件,如果失败或者数据库插入部分失败直接回滚,即删除该文件,如果成功则提交事务,即将该文件重命名为你需要的正式文件名字(重命名基本不会失败,如果失败了比如断电,那就是特殊情况了)。同理删除文件是先将文件重命名做一个临时文件而不是直接删除,然后数据库部分删除失败的话回滚事务,即将该文件重命名成原来的,如果成功则提交事务,即删除临时文件。

  和数据库搭配使用异常的逻辑判断需要谨慎,比如删除文件应先对数据库操作进行判断,如果先对文件操作进行判断,加入成功了直接提交事务即删除了临时文件,数据库部分失败了文件是没办法回滚的。

我这里用的是spriingBoot,如果用的别的看情况做修改即可,这里需要四个类:

SftpProperties:这个是sftp连接文件服务器的各项属性,各属性需要配置到springBoot配置文件中,也可以换种方法获取到即可。

 import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; @Component
public class SftpProperties {
@Value("${spring.sftp.ip}")
private String ip;
@Value("${spring.sftp.port}")
private int port;
@Value("${spring.sftp.username}")
private String username;
@Value("${spring.sftp.password}")
private String password; public String getIp() {
return ip;
} public void setIp(String ip) {
this.ip = ip;
} public int getPort() {
return port;
} public void setPort(int port) {
this.port = port;
} public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPassword() {
return password;
} public void setPassword(String password) {
this.password = password;
} @Override
public String toString() {
return "SftpConfig{" +
"ip='" + ip + '\'' +
", port=" + port +
", username='" + username + '\'' +
", password='******'}";
}
}

SftpClient:这个主要通过sftp连接文件服务器并读取数据。

 import com.jcraft.jsch.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import java.io.*; @Component
public class SftpClient implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(SftpClient.class);
private Session session; //通过sftp连接服务器
public SftpClient(SftpProperties config) throws JSchException {
JSch.setConfig("StrictHostKeyChecking", "no");
session = new JSch().getSession(config.getUsername(), config.getIp(), config.getPort());
session.setPassword(config.getPassword());
session.connect();
} public Session getSession() {
return session;
} public ChannelSftp getSftpChannel() throws JSchException {
ChannelSftp channel = (ChannelSftp) session.openChannel("sftp");
channel.connect();
return channel;
} /**
* 读取文件内容
* @param destFm 文件绝对路径
* @return
* @throws JSchException
* @throws IOException
* @throws SftpException
*/
public byte[] readBin(String destFm) throws JSchException, IOException, SftpException {
ChannelSftp channel = (ChannelSftp) session.openChannel("sftp");
channel.connect();
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
channel.get(destFm, outputStream);
return outputStream.toByteArray();
} finally {
channel.disconnect();
}
} /**
* 退出登录
*/
@Override
public void close() throws Exception {
try {
this.session.disconnect();
} catch (Exception e) {
//ignore
}
}
}

SftpTransaction:这个主要是对文件的操作

 import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSchException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID; @Component
public class SftpTransaction {
private static final Logger LOGGER = LoggerFactory.getLogger(SftpTransaction.class);
private final String transactionId; // 事务唯一id
private final ChannelSftp channelSftp;
private int opType = -1; // 文件操作标识 1 添加文件 2 删除文件
private List<String> opFiles = new ArrayList<>(5); public SftpTransaction(SftpClient client) throws JSchException {
this.transactionId = StringUtils.replace(UUID.randomUUID().toString(), "-", "");
this.channelSftp = client.getSftpChannel();
} // 根据文件名和事务id创建临时文件
private String transactionFilename(String transactionId, String filename, String path) {
return String.format("%stransact-%s-%s", path, transactionId, filename);
} // 根据路径反推文件名
private String unTransactionFilename(String tfm, String path) {
return path + StringUtils.split(tfm, "-", 3)[2];
} /**
* 添加文件
* @param contents 存放文件内容
* @param path 文件绝对路径(不包含文件名)
* @throws Exception
*/
public void create(List<Pair<String, byte[]>> contents, String path) throws Exception {
if (this.opType == -1) {
this.opType = 1;
} else {
throw new IllegalStateException();
}
for (Pair<String, byte[]> content : contents) {
// 获取content里的数据
try (ByteArrayInputStream stream = new ByteArrayInputStream(content.getValue())) {
// 拼接一个文件名做临时文件
String destFm = this.transactionFilename(this.transactionId, content.getKey(), path);
this.channelSftp.put(stream, destFm);
this.opFiles.add(destFm);
}
}
} /**
* 删除文件
* @param contents 存放要删除的文件名
* @param path 文件的绝对路径(不包含文件名)
* @throws Exception
*/
public void delete(List<String> contents, String path) throws Exception {
if (this.opType == -1) {
this.opType = 2;
} else {
throw new IllegalStateException();
}
for (String name : contents) {
String destFm = this.transactionFilename(this.transactionId, name, path);
this.channelSftp.rename(path+name, destFm);
this.opFiles.add(destFm);
}
} /**
* 提交事务
* @param path 绝对路径(不包含文件名)
* @throws Exception
*/
public void commit(String path) throws Exception {
switch (this.opType) {
case 1:
for (String fm : this.opFiles) {
String destFm = this.unTransactionFilename(fm, path);
//将之前的临时文件命名为真正需要的文件名
this.channelSftp.rename(fm, destFm);
}
break;
case 2:
for (String fm : opFiles) {
//删除这个文件
this.channelSftp.rm(fm);
}
break;
default:
throw new IllegalStateException();
}
this.channelSftp.disconnect();
} /**
* 回滚事务
* @param path 绝对路径(不包含文件名)
* @throws Exception
*/
public void rollback(String path) throws Exception {
switch (this.opType) {
case 1:
for (String fm : opFiles) {
// 删除这个文件
this.channelSftp.rm(fm);
}
break;
case 2:
for (String fm : opFiles) {
String destFm = this.unTransactionFilename(fm, path);
// 将文件回滚
this.channelSftp.rename(fm, destFm);
}
break;
default:
throw new IllegalStateException();
}
this.channelSftp.disconnect();
}
}

SftpTransactionManager:这个是对事务的操作。

 import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; @Component
public class SftpTransactionManager {
@Autowired
private SftpClient client; //开启事务
public SftpTransaction startTransaction() throws Exception {
return new SftpTransaction(client);
} /**
* 提交事务
* @param transaction
* @param path 绝对路径(不包含文件名)
* @throws Exception
*/
public void commitTransaction(SftpTransaction transaction, String path) throws Exception {
transaction.commit(path);
} /**
* 回滚事务
* @param transaction
* @param path 绝对路径(不包含文件名)
* @throws Exception
*/
public void rollbackTransaction(SftpTransaction transaction, String path) throws Exception {
transaction.rollback(path);
}
}

SftpTransactionTest:这是一个测试类,使用之前可以先行测试是否可行,有问题可以评论

 import com.springcloud.utils.sftpUtil.SftpTransaction;
import com.springcloud.utils.sftpUtil.SftpTransactionManager;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Test; import java.util.ArrayList;
import java.util.List; /**
* 测试文件事务管理
*/
public class SftpTransactionTest { //创建文件
@Test
public static void createFile() throws Exception {
// 定义一个存放文件的绝对路径
String targetPath = "/data/file/";
//创建一个事务管理实例
SftpTransactionManager manager = new SftpTransactionManager();
SftpTransaction sftpTransaction = null;
try {
//开启事务并返回一个事务实例
sftpTransaction = manager.startTransaction();
//创建一个存放要操作文件的集合
List<Pair<String, byte[]>> contents = new ArrayList<>();
ImmutablePair aPair = new ImmutablePair<>("file_a", "data_a".getBytes()); //file_a是文件a的名字,data_a是文件a的内容
ImmutablePair bPair = new ImmutablePair<>("file_b", "data_b".getBytes());
ImmutablePair cPair = new ImmutablePair<>("file_c", "data_c".getBytes());
contents.add(aPair);
contents.add(bPair);
contents.add(cPair);
// 将内容进行事务管理
sftpTransaction.create(contents, targetPath);
// 事务提交
manager.commitTransaction(sftpTransaction, targetPath);
}catch (Exception e) {
if (sftpTransaction != null) {
// 发生异常事务回滚
manager.rollbackTransaction(sftpTransaction, targetPath);
}
throw e;
}
} //删除文件
@Test
public void deleteFile() throws Exception {
// 定义一个存放文件的绝对路径
String targetPath = "/data/file/";
//创建一个事务管理实例
SftpTransactionManager manager = new SftpTransactionManager();
SftpTransaction sftpTransaction = null;
try {
//开启事务并返回一个事务实例
sftpTransaction = manager.startTransaction();
List<String> contents = new ArrayList<>();
contents.add("file_a"); // file_a要删除的文件名
contents.add("file_b");
contents.add("file_c");
sftpTransaction.delete(contents, targetPath);
manager.commitTransaction(sftpTransaction, targetPath);
} catch (Exception e) {
//回滚事务
if (sftpTransaction != null) {
manager.rollbackTransaction(sftpTransaction, targetPath);
}
throw e;
}
}
}

这是对于sftp文件操作的依赖,其他的依赖应该都挺好。

  <dependency>  <groupId>com.jcraft</groupId>  <artifactId>jsch</artifactId>  </dependency> 

  ok,到这里已经完了,之前有需要写文件事务管理的时候只找到一个谷歌的包可以完成(包名一时半会忘记了),但是与实际功能还有些差别,所以就根据那个源码自己改了改,代码写的可能很一般,主要也是怕以后自己用忘记,就记下来,如果刚好能帮到有需要的人,那就更好。哪位大神如果有更好的方法也请不要吝啬,传授一下。(抱拳)