前言:
公司業務需要,現將系統中與文檔上傳下載預覽相關的服務接口從阿里雲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(); 即可解決