应用场景:给定一个网址,输入网址后立即显示预览文件。
实现思路:
1、将文件下载到本地,存储到某个指定目录
2、进行文件转换,此处是重点
3、进行文件展示
首先,搭建一个springboot项目,搭建完毕后项目结构如图:
目录结构中static存放的是静态资源,templates中存放页面。其中配置文件里存放我们的配置信息:application.yml
其中tmp:root下面是转换后文件的存放位置,rootTemp则是下载文件的临时存放位置,后续会有定时器定时删除该目录下的内容,soffice:home配置的是openoffice的安装目录,因为office文件的转换要用到openoffice。其中的type则是可以预览的文件类型。好了,搭建完毕之后开始搭建Service层,Dao层(可用可不用),Controller层。整体搭建完毕之后如下图:
相关依赖pom.xml
4.0.0
file-conventer
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
1.5.9.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
net.sourceforge.jchardet
jchardet
1.0
org.artofsolving.jodconverter
jodconverter-core
3.0.0
org.apache.commons
commons-compress
1.11
org.apache.commons
commons-compress
1.9
com.github.junrar
junrar
0.7
com.google.guava
guava
19.0
commons-codec
commons-codec
2.0-SNAPSHOT
net.lingala.zip4j
zip4j
1.3.2
org.springframework
spring-context-support
4.3.10.RELEASE
org.freemarker
freemarker
2.3.25-incubating
net.sf.json-lib
json-lib
2.4
com.google.code.gson
gson
2.3.1
commons-io
commons-io
1.4
org.openoffice
juh
3.2.1
org.openoffice
ridl
3.2.1
org.openoffice
unoil
3.2.1
commons-cli
commons-cli
1.1
true
org.hyperic
sigar
1.6.5.132
true
org.json
json
20090211
true
org.testng
testng
6.0.1
test
org.springframework.boot
spring-boot-maven-plugin
项目开始,要做的第一件事是下载文件,这里下载文件有一个要注意的地方
我没有截全,因为文件的下载在这篇文章里不是重点,所以在这里不做详细记录,只要最后将文件存储到临时目录就行
文件下载完之后在进行文章的转换,这里我的转换规则:
1、图片不进行转换
2、文本文件转换编码为utf-8
3、office文件选择将word文档转为pdf文件,xls表格和ppt文件转为html文件,因为表格和ppt文件转换为pdf格式之后不太美观,所以选择将ppt转为html格式,后期取图片展示就可以了
4、压缩文件先解压到本地,然后运用Ztree插件做前端的展示。
由于涉及文件的操作,所以在这里一定要注意文件流的运用,用完了一定要及时关闭,还有文件的其他操作也要注意,比如文件地址的指向。当时我就是因为一个文件地址未仍指向一个操作中的目录导致后面临时文件的删除出错。
下面开始文件的转换,重点记录文本文件以及压缩文件的转换,当时在这花了较长时间,office文件的转换可以参照其他人写的。
文本文件的转换(这里要注意的是文本文件涉及到文件的编码问题,这里采用文本编码探测器进行探测):
文本探测器如下:
import org.mozilla.intl.chardet.nsDetector;
import org.mozilla.intl.chardet.nsICharsetDetectionObserver;
import java.io.*;
/**
* Created by asus on 2017/12/28.
*/
public class FileCharsetDetector {
/**
* 传入一个文件(File)对象,检查文件编码
*
* @param file
* File对象实例
* @return 文件编码,若无,则返回null
* @throws FileNotFoundException
* @throws IOException
*/
public static Observer guessFileEncoding(File file)
throws FileNotFoundException, IOException {
return guessFileEncoding(file, new nsDetector());
}
/**
*
* 获取文件的编码
* @param file
* File对象实例
* @param languageHint
* 语言提示区域代码 @see #nsPSMDetector ,取值如下:
* 1 : Japanese
* 2 : Chinese
* 3 : Simplified Chinese
* 4 : Traditional Chinese
* 5 : Korean
* 6 : Dont know(default)
*
*
* @return 文件编码,eg:UTF-8,GBK,GB2312形式(不确定的时候,返回可能的字符编码序列);若无,则返回null
* @throws FileNotFoundException
* @throws IOException
*/
public static Observer guessFileEncoding(File file, int languageHint)
throws FileNotFoundException, IOException {
return guessFileEncoding(file, new nsDetector(languageHint));
}
/**
* 获取文件的编码
*
* @param file
* @param det
* @return
* @throws FileNotFoundException
* @throws IOException
*/
private static Observer guessFileEncoding(File file, nsDetector det)
throws FileNotFoundException, IOException {
// new Observer
Observer observer = new Observer();
// set Observer
// The Notify() will be called when a matching charset is found.
det.Init(observer);
BufferedInputStream imp = new BufferedInputStream(new FileInputStream(
file));
byte[] buf = new byte[1024];
int len;
boolean done = false;
boolean isAscii = false;
while ((len = imp.read(buf, 0, buf.length)) != -1) {
// Check if the stream is only ascii.
isAscii = det.isAscii(buf, len);
if (isAscii) {
break;
}
// DoIt if non-ascii and not done yet.
done = det.DoIt(buf, len, false);
if (done) {
break;
}
}
imp.close();
det.DataEnd();
if (isAscii) {
observer.encoding = "ASCII";
observer.found = true;
}
if (!observer.isFound()) {
String[] prob = det.getProbableCharsets();
// // 这里将可能的字符集组合起来返回
// for (int i = 0; i < prob.length; i++) {
// if (i == 0) {
// encoding = prob[i];
// } else {
// encoding += "," + prob[i];
// }
// }
if (prob.length > 0) {
// 在没有发现情况下,去第一个可能的编码
observer.encoding = prob[0];
} else {
return null;
}
}
return observer;
}
/**
* @Description: 文件字符编码观察者,但判断出字符编码时候调用
*/
public static class Observer implements nsICharsetDetectionObserver {
/**
* @Fields encoding : 字符编码
*/
private String encoding = null;
/**
* @Fields found : 是否找到字符集
*/
private boolean found = false;
@Override
public void Notify(String charset) {
this.encoding = charset;
this.found = true;
}
public String getEncoding() {
return encoding;
}
public boolean isFound() {
return found;
}
@Override
public String toString() {
return "Observer [encoding=" + encoding + ", found=" + found + "]";
}
}
}
压缩文件转换(因为前端要生成文件树,所以在这里要先进行文件解压,在进行文件的读取,最终要生成的是一段字符串里面包含所有文件的信息):
解压文件的操作这里也不详细记录了,很多网上的资料,这里记录一下文件树的生成,首先我们定义一个文件节点,里面包含子文件,文件名称,判断是否为文件夹以及文件绝对路径:
/**
* 文件节点(区分文件上下级)
*/
public static class FileNode{
private String originName;
private boolean directory;
private String fullPath;
private List childList;
public FileNode(String originName, List childList, boolean directory, String fullPath) {
this.originName = originName;
this.childList = childList;
this.directory = directory;
this.fullPath = fullPath;
}
public String getFullPath() {
return fullPath;
}
public void setFullPath(String fullPath) {
this.fullPath = fullPath;
}
public List getChildList() {
return childList;
}
public void setChildList(List childList) {
this.childList = childList;
}
@Override
public String toString() {
try {
return new ObjectMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return "";
}
}
public String getOriginName() {
return originName;
}
public void setOriginName(String originName) {
this.originName = originName;
}
public boolean isDirectory() {
return directory;
}
public void setDirectory(boolean directory) {
this.directory = directory;
}
}
/**
* 通过递归得到某一路径下所有的目录及其文件
*/
public static List getFiles(String filePath){
File root = new File(filePath);
File[] files = root.listFiles();
String originName = "";
boolean isDirectory = false;
String fullPath = "";
List fileNodes = new ArrayList<>();
for(File file:files){
List childList = new ArrayList<>();
if(file.isDirectory()){
isDirectory = true;
originName = file.getName();
fullPath = file.getAbsolutePath();
childList = getFiles(file.getPath());
} else {
originName = file.getName();
isDirectory = false;
fullPath = file.getAbsolutePath();
}
// 进行转义,否则json解析不了
fullPath = fullPath.replace("\\", "/");
FileNode fileNode = new FileNode(originName, childList, isDirectory, fullPath);
fileNodes.add(fileNode);
}
return fileNodes;
}
这样我们就得到了文件树,前端的展示就简单多了,后面只需要把文件树转为字符串传到前端j就行了,注意这里的文件绝对路径的写法,默认是生成“\\”,这里要换成“/”。到这步,文件的转换基本就差不多了,剩下我们要做的就是文件的展示。这里还有一个地方需要注意的是,生成文件存储目录的时候增加一层目录用于区分唯一文件,这里我采用的是取文件的hash值,作为文件的上级存储目录,这样就不会有重复的文件目录了。
文件展示 :前面一直没有说Controller层如何写,这里开始说明
原理就是转换完文件之后再定位到文件存放的目录,将本地文件以流的方式输出到页面。
这样写完之后便可以写页面了,页面显示规则:1、图片的显示可以用viewer.js插件(一个图片显示器,支持主流的图片显示操作),我这里直接写到页面上,后续再加上。2、pdf文件的显示可以用pdf.js插件。3、压缩文件显示用Ztree插件。4、ppt文件的显示这里推荐一个开源js,个人觉得还不错,点击打开链接。
最后,显示效果如下:
报表:
PPT文件:
图片:
个人项目可以采用和我一样的方式,写的有点乱,有问题欢迎在底部留言。最后附上项目地址:https://github.com/Chenchicheng/file_viewer。