前言:
公司业务需要,现将系统中与文档上传下载预览相关的服务接口从阿里云OSS转移到本地化部署的文档服务器中。现使用FTP文档服务器的功能替换原有接口来完成实现,实现过程记录
目录
一:FTP的搭建:
需要在Linux系统中搭建FTP服务,实现上传下载功能
1.1 准备环境、创建账号
1. 准备一台Linux7.0一上版本的服务器,查询是否已经安装FTP服务:rpm -qa |grep vsftpd
如果查询 无结果,则表示该服务器未安装FTP服务。执行命令:yum -y install vsftpd
安装出现:complete表示安装已经完成,默认端口为21
2. 启动FTP:systemctl start vsftpd.service
查询FTP启动状态:systemctl status vsftpd.service
关闭FTP:systemctl stop vsftpd.service
重启FTP: systemctl restart vsftpd.service
设置FTP开机自启动:chkconfig vsftpd on
3. 创建一个FTP操作的目录:mkdir -p /opt/pms/ftptest
4. 创建FTP用户:useradd -d /opt/pms/ftptest -g ftp -s /sbin/nologin ftptest
-g ftp 表示该用户属于ftp分组 (ftp分组是内置的,本来就存在,不需要自己创建)
-s /sbin/nologin 表示这个用户不能用来登录secureCRT这样的客户端。 这种不能登陆的用户又叫做虚拟用户
创建过程给出的警告信息是正常的,不用理会
5. 设置目录权限:
chown -R ftptest /home/wwwroot/ftptest #把目录/home/wwwroot/ftptest的拥有者设置为ftptest
chmod -R 775 /home/wwwroot/ftptest #使ftptest用户拥有这个目录的读写权限
6. 设置密码:passwd ftptest
1.2 用户配置、端口配置、用户鉴权
1. 给创建的用户去掉匿名登录(默认情况下vsftpd服务器是允许匿名登陆的,这样非常不安全,所以要把这个选项关闭掉)
打开配置文件:vi /etc/vsftpd/vsftpd.conf
将原先的anonymous_enable=YES ->>>>> anonymous_enable=NO,并保存退出:wq
2. 限制用户访问(如果不做限制,那么使用ftptest登陆之后可以切换到其他敏感目录去,比如切换到/usr目录去,这样就存在巨大的安全隐患)
打开配置文件:vi /etc/vsftpd/vsftpd.conf
#chroot_list_enable=YES
# (default follows)
#chroot_list_file=/etc/vsftpd.chroot_list
修改为:
chroot_list_enable=YES #表示对用户的访问进行限制
# (default follows)
chroot_list_file=/etc/vsftpd/chroot_list #用户清单。在该列表中的用户将允许访问
3. 编辑用户清单,将我们需要的用户添加至清单
命令:vi /etc/vsftpd/chroot_list
打开后是空的,然后将需要的用户添加进去即可。例如:ftptest
4. 允许写权限
一旦某个用户被限制访问了,那么默认情况下,该用户的写权限也被剥夺了。 这就导致ftp客户端连接上服务器之后无法上传文件。
通过命令:vi /etc/vsftpd/vsftpd.conf
最后一行添加:allow_writeable_chroot=YES
5. 开放端口
21端口自是用来监听客户端请求的,还需要开放端口,来完成服务端与客户端传输数据的端口
配置文件:vi /etc/vsftpd/vsftpd.conf
在最后添加:
pasv_enable=YES
pasv_min_port=30000
pasv_max_port=30010
6. 用户鉴权(因为用户 ftptest 是 nologin的,所以存在鉴权的问题)
vi /etc/pam.d/vsftpd
注释掉:#auth required pam_shells.so
保存即可
7. 重启FTP,通过工具连接该服务器的FTP,即可完成文件的上传下载功能
二. 接口实现
搭建的FTP服务器,想通过java接口的形式完成上传下载删除的功能,以满足本地化文档服务器的需求。这里使用的是FTPClient对象的相关API完成。
1. maven依赖:
<!--FTP--> <dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.3</version> </dependency>
2. 上传接口
(将文件通过前端上传,接口接受后存放在FTP 服务器)
/**
* 文件上传
*
* @param fileName : 上传的文件名
* @param inputStream 文件的流文件
* @return map fileName: 文件原本名 | ftpFileName:上传至FTP文件名 | status:true:成功、false:失败 | path:上传的文档路径
*/
@Override
public Map<String, Object> uploadFile(String fileName, InputStream inputStream) {
Map<String, Object> map = new HashMap<>();
map.put("fileName", fileName);
//将文件名添加时间戳
fileName = DateUtil.dateToUnixSecond(new Date()) + fileName;
map.put("ftpFileName", fileName);
//文件上传到FTP的路径
String path = ftpConfig.getFilePath()+ DateUtil.getDateStr();
String fullPath = path;
boolean flag = false;
//1. 创建ftpclient对象
FTPClient ftpClient = new FTPClient();
//2. 设置编码 格式
ftpClient.setControlEncoding("UTF-8");
try {
//连接FTP服务器
ftpClient.connect(ftpConfig.getHostName(), ftpConfig.getPort());
//登录FTP服务器
ftpClient.login(ftpConfig.getUserName(), ftpConfig.getPassWord());
//是否成功登录FTP服务器
int replyCode = ftpClient.getReplyCode();
if(!FTPReply.isPositiveCompletion(replyCode)){
map.put("status", flag);
return map;
}
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
//makeDirectory()方法创建目录,返回true表示服务器中不存在此目录,false表示已经存在此目录
ftpClient.makeDirectory(path);
//变更路径
ftpClient.changeWorkingDirectory(path);
//此段代码是做上传文件分文件夹处理
/*FTPFile[] ftpFiles = ftpClient.listFiles();
//当该文件夹的文件数量大于5000时,则创建新的文件,路径为yyyyMMdd_1--yyyyMMdd_10 共十个文件夹
if(ftpFiles.length >= MAX_SIZE) {
for (int i = 1; i <= 10; i++) {
fullPath = path + "_"+ i;
ftpClient.makeDirectory(fullPath);
ftpClient.changeWorkingDirectory(fullPath);
FTPFile[] charFtpFiles = ftpClient.listFiles();
if(charFtpFiles.length < MAX_SIZE) {
break;
}
}
}*/
map.put("path", fullPath);
ftpClient.storeFile(fileName, inputStream);
inputStream.close();
ftpClient.logout();
flag = true;
map.put("status", flag);
} catch (Exception e) {
GwsLogger.error("文件上传失败!,文件名={}", map.get("fileName"));
map.put("status", flag);
return map;
} finally{
if(ftpClient.isConnected()){
try {
ftpClient.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return map;
}
3. 下载接口
(通过文件名+文件路径参数进行下载,可将文件名与路径存放在数据库, 前端点击下载时参数传过来直接下载,此接口可通过response返回,在前端请求直接弹出下载框)
/**
* 下载文件
*
* @param filename :下载的文件名
* @param path :下载的文件路径
* @param response 响应体(直接弹窗下载)
*/
@Override
public boolean downloadFile(String filename, String path, HttpServletResponse response) {
filename = filename.trim();
boolean flag = false;
FTPClient ftpClient = new FTPClient();
try {
//连接FTP服务器
ftpClient.connect(ftpConfig.getHostName(), ftpConfig.getPort());
//登录FTP服务器
ftpClient.login(ftpConfig.getUserName(), ftpConfig.getPassWord());
//验证FTP服务器是否登录成功
int replyCode = ftpClient.getReplyCode();
if(!FTPReply.isPositiveCompletion(replyCode)){
return flag;
}
ftpClient.setControlEncoding("UTF-8");
//切换FTP目录,按照ISO_8859_1编码
ftpClient.changeWorkingDirectory(new String(path.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
FTPFile[] ftpFiles = ftpClient.listFiles();
if(null == ftpFiles || ftpFiles.length == 0) {
return false;
}
for(FTPFile file : ftpFiles){
if(filename.equalsIgnoreCase(file.getName())){
File localFile = new File("/" + file.getName());
OutputStream os = new FileOutputStream(localFile);
String ftpFileName = new String(file.getName().getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
ftpClient.retrieveFile(ftpFileName, os);
InputStream input = new FileInputStream(localFile);
byte[] data = IOUtils.toByteArray(input);
response.setContentType("application/binary;charset=UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(filename, "utf-8"));
response.setContentLength(data.length);
response.getOutputStream().write(data);
response.getOutputStream().flush();
response.getOutputStream().close();
os.flush();
os.close();
input.close();
localFile.delete();
}
}
ftpClient.logout();
flag = true;
} catch (Exception e) {
e.printStackTrace();
} finally{
if(ftpClient.isConnected()){
try {
ftpClient.logout();
} catch (IOException e) {
}
}
}
return flag;
}
4. 删除接口
(通过文件路径与文件名进行删除)
/**
* 删除文件
* @param path :文件路径
* @param filename :文件名
*/
@Override
public boolean deleteFile(String path, String filename) {
boolean flag = false;
FTPClient ftpClient = new FTPClient();
try {
//连接FTP服务器
ftpClient.connect(ftpConfig.getHostName(), ftpConfig.getPort());
//登录FTP服务器
ftpClient.login(ftpConfig.getUserName(), ftpConfig.getPassWord());
//验证FTP服务器是否登录成功
int replyCode = ftpClient.getReplyCode();
if(!FTPReply.isPositiveCompletion(replyCode)){
return flag;
}
ftpClient.setControlEncoding("UTF-8");
//切换FTP目录
ftpClient.changeWorkingDirectory(new String(path.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
ftpClient.dele(new String(filename.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
ftpClient.logout();
flag = true;
} catch (Exception e) {
e.printStackTrace();
} finally{
if(ftpClient.isConnected()){
try {
ftpClient.logout();
} catch (IOException e) {
}
}
}
return flag;
}
5. 预览接口
服务器安装openoffice,通过openoffice完成文档的预览功能。安装openoffice方法参考博客:https://www.cnblogs.com/Oliver-rebirth/p/Linux_openOffice.html
public RetResult filePreview(HttpServletRequest request, String filename, String path, HttpServletResponse response) {
Map<String, Object> map = new HashMap<>();
filename = filename.trim();
String pdfFileName = PrimaryKeyGeneratorTool.generateKey32() + filename;
boolean flag = false;
boolean overUpload = false;
FTPClient ftpClient = new FTPClient();
try {
//连接FTP服务器
ftpClient.connect(ftpConfig.getHostName(), ftpConfig.getPort());
//登录FTP服务器
ftpClient.login(ftpConfig.getUserName(), ftpConfig.getPassWord());
//验证FTP服务器是否登录成功
int replyCode = ftpClient.getReplyCode();
if(!FTPReply.isPositiveCompletion(replyCode)){
return RetResult.setError("error", "文档服务器连接失败,请联系管理员");
}
ftpClient.setControlEncoding("UTF-8");
//切换FTP目录,按照ISO_8859_1编码
ftpClient.changeWorkingDirectory(new String(path.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
FTPFile[] ftpFiles = ftpClient.listFiles();
if(null == ftpFiles || ftpFiles.length == 0) {
return RetResult.setError("empty", "该文档不存在!");
}
for(FTPFile file : ftpFiles){
if(filename.equalsIgnoreCase(file.getName())){
flag = true;
File localFile = new File("/opt/pms/ftptest/" + file.getName());
OutputStream os = new FileOutputStream(localFile);
String ftpFileName = new String(file.getName().getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
ftpClient.retrieveFile(ftpFileName, os);
InputStream input = new FileInputStream(localFile);
String type = filename.substring(filename.lastIndexOf("."));
if(".xls".equals(type) || ".xlsx".equals(type)){
String htmlString = ExcelToHTML.readExcelToHtmlString(input,true);
map.put("htmlString", htmlString);
localFile.delete();
return RetResult.setSuccess(map);
}else if(".pdf".equals(type)) {
FileInputStream in = new FileInputStream(localFile);
response.setContentType("application/pdf");
OutputStream out = response.getOutputStream();
byte[] b = new byte[1024];
while ((in.read(b))!=-1) {
out.write(b);
}
out.flush();
in.close();
out.close();
}else{
Doc2PDFUtil doc2PDFUtilInstance = Doc2PDFUtil.getDoc2PDFUtilInstance();
FileInputStream in = doc2PDFUtilInstance.file2pdfFile(input,type,request);
response.setContentType("application/pdf");
OutputStream out = response.getOutputStream();
byte[] b = new byte[1024];
while ((in.read(b))!=-1) {
out.write(b);
}
out.flush();
in.close();
out.close();
in.close();
}
os.flush();
os.close();
localFile.delete();
}
}
ftpClient.logout();
if(!flag) {
return RetResult.setError("emply", "文档不存在,预览失败!");
}
} catch (Exception e) {
e.printStackTrace();
} finally{
if(ftpClient.isConnected()){
try {
ftpClient.logout();
} catch (IOException e) {
GwsLogger.error("IO异常");
}
}
}
return RetResult.setSuccess("ok");
}
public static FileInputStream file2pdfFile(InputStream fromFileInputStream, String type, HttpServletRequest request) throws IOException {
final String PATH= request.getSession().getServletContext().getRealPath(File.separator);
GwsLogger.info(PATH);
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String timesuffix = sdf.format(date);
String docFileName = null;
String htmFileName = null;
if(".doc".equals(type)){
docFileName = "doc_" + timesuffix + ".doc";
htmFileName = "doc_" + timesuffix + ".pdf";
}else if(".docx".equals(type)){
docFileName = "docx_" + timesuffix + ".docx";
htmFileName = "docx_" + timesuffix + ".pdf";
}else if(".xls".equals(type)){
docFileName = "xls_" + timesuffix + ".xls";
htmFileName = "xls_" + timesuffix + ".pdf";
}else if(".ppt".equals(type)){
docFileName = "ppt_" + timesuffix + ".ppt";
htmFileName = "ppt_" + timesuffix + ".pdf";
}else if(".xlsx".equals(type)){
docFileName = "xls_" + timesuffix + ".xls";
htmFileName = "xls_" + timesuffix + ".pdf";
} else{
throw new ImplException("1","当前文件格式不支持转换为pdf格式");
}
File pdfOutputFile = new File(PATH+ File.separatorChar + htmFileName);
File docInputFile = new File(PATH + File.separatorChar + docFileName);
if (pdfOutputFile.exists())
pdfOutputFile.delete();
pdfOutputFile.createNewFile();
if (docInputFile.exists())
docInputFile.delete();
docInputFile.createNewFile();
/*** 由fromFileInputStream构建输入文件 */
try {
OutputStream os = new FileOutputStream(docInputFile);
int bytesRead = 0;
byte[] buffer = new byte[1024 * 8];
while ((bytesRead = fromFileInputStream.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
fromFileInputStream.close();
} catch (IOException e) {
}
// 连接服务
OpenOfficeConnection connection = new SocketOpenOfficeConnection(8900);
try {
connection.connect();
} catch (ConnectException e) {
GwsLogger.error("文件转换出错,请检查OpenOffice服务是否启动。");
}
FileInputStream pdfStream=null;
// convert 转换
try {
DocumentConverter converter = new StreamOpenOfficeDocumentConverter(connection);
converter.convert(docInputFile, pdfOutputFile);
connection.disconnect();
pdfStream=new FileInputStream(pdfOutputFile);
return pdfStream;
}catch (Exception e){
GwsLogger.error("文件转换异常"+e.getMessage());
}finally {
// 转换完之后删除word文件
docInputFile.delete();
pdfOutputFile.delete();
}
return null;
}
6. 接口过程中遇见的问题
①
问题:切换FTP目录失败、创建文件目录失败:
ftpClient.makeDirectory、ftpClient.changeWorkingDirectory方法失败,返回报错550
原因:因为FTP的账号配置问题,在Linux中FTP的配置文件做了用户限制访问,也就是打开了chroot_list_enable=YES配置,并在chroot_list_file=/etc/vsftpd/chroot_list列表中将当前接口登录的FTP账号添加了进去,导致FTP目录创建/切换失败
解决方案:将FTP的用户限制访问配置接触即可,详情可看上面的用户限制配置
②
问题:中文文件名下载后内容为空
原因:因为获取FTP文件名时,编码问题导致。
解决方案:
1. 将FTP的编码方式切换为ftpClient.setControlEncoding("UTF-8");
2. 切换路径时,将路径的编码方式修改为UTF-8以及.ISO_8859_1
ftpClient.changeWorkingDirectory(new String("/opt/pms/ftptest/".getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
3. 获取文件名时,将文件名的编码方式设置为UTF-8
String ftpFileName = new String(file.getName().getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
ftpClient.retrieveFile(ftpFileName, os);
③
问题:FTP调用dele()方法失败,提示550 Delete operation failed
原因:因为中文文件名删除失败
解决方案:将文件名通过UTF-8进行编码以及ISO_8859_1即可完成删除方法
ftpClient.dele(new String(filename.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
④
问题:获取上传类型为 MultipartFile的文件名
解决方案:fileName = file.getOriginalFilename();
问题:查询FTP目录是否存在
解决方案:调用ftpClient.makeDirectory(目录地址); 返回结zd果true或false,true代表创建成功,即目录不存在,false表示创建失败,表示该路径已经存在了。
⑤
问题:doc转PDF后中文不显示,只显示英文
解决方案:将windows系统下的中文字体文件(C:\Windows\Fonts),放到/usr/share/fonts下,必须重启openoffice。
⑥
问题:上传时,报错500 Illegal PORT command
原因:是因为上传时,被动模式没有开启
解决方案:将FTPClient设置为被动模式:ftpClient.enterLocalPassiveMode(); 即可解决