文件预览
详细介绍如何配置和开发 Free FS 的文件预览插件。
架构概述
Free FS 采用策略模式的插件化预览架构,支持通过 Spring 依赖注入机制动态加载预览策略。系统默认实现了多种文件类型的预览策略(图片、PDF、Office、代码、Markdown、音视频等),每种文件类型都有对应的预览策略实现。
核心组件
- PreviewStrategy: 预览策略接口,定义了所有预览策略必须实现的方法
- AbstractPreviewStrategy: 抽象预览策略,提供通用功能和默认实现
- PreviewStrategyManager: 预览策略管理器,负责管理和选择预览策略
- IConverter: 文件转换器接口,用于文件格式转换(如 Office 转 PDF)
- PreviewContext: 预览上下文,封装预览所需的信息
- FilePreviewConfig: 预览配置类,管理预览相关配置
策略加载流程
- 系统启动时,Spring 自动扫描所有标注了
@Component的PreviewStrategy实现类 - 所有策略被注入到
PreviewStrategyManager中 - 策略按优先级(
getPriority())排序,数字越小优先级越高 - 当需要预览文件时,管理器根据文件类型选择第一个支持该类型的策略
- 如果所有策略都不支持,则使用
UnsupportedPreviewStrategy作为兜底策略
文件类型支持
系统默认支持以下多种文件类型的预览:
- 图片: jpg, jpeg, png, gif, bmp, webp, svg, tif, tiff
- 文档: pdf, doc, docx, xls, xlsx, csv, ppt, pptx
- 文本/代码: txt, log, ini, properties, yaml, yml, conf, java, js, jsx, ts, tsx, py, c, cpp, h, hpp, cc, cxx, html, css, scss, sass, less, vue, php, go, rs, rb, swift, kt, scala, json, xml, sql, sh, bash, bat, ps1, cs, toml
- Markdown: md, markdown
- 音视频: mp4, avi, mkv, mov, wmv, flv, webm, mp3, wav, flac, aac, ogg, m4a, wma
- 压缩包: zip, rar, 7z, tar, gz, bz2 (支持查看目录结构)
- 其他: drawio
如何快速创建一个预览策略?
方式一:简单预览(无需转换)
对于可以直接在浏览器中预览的文件类型(如图片、PDF、视频、音频等),只需继承 AbstractPreviewStrategy 并实现必要的方法。
方式二:需要格式转换的预览
对于需要转换格式才能预览的文件(如 Office 文档转 PDF),需要实现 IConverter 接口,并在策略中指定转换器。
实现步骤
步骤 1:创建预览策略类
创建一个继承 AbstractPreviewStrategy 的策略类:
package com.xddcodec.fs.framework.preview.strategy.impl;
import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
import com.xddcodec.fs.framework.preview.core.PreviewContext;
import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
/**
* 你的文件类型预览策略
*/
@Slf4j
@Component
public class YourFilePreviewStrategy extends AbstractPreviewStrategy {
@Override
public boolean support(FileTypeEnum fileType) {
// 返回该策略支持的文件类型
return fileType == FileTypeEnum.YOUR_TYPE;
}
@Override
public String getTemplatePath() {
// 返回预览模板路径(相对于 templates 目录)
return "preview/your-template";
}
@Override
protected void fillSpecificModel(PreviewContext context, Model model) {
// 填充模板所需的特定数据
// 基础数据(fileName、fileSize、fileType、extension、streamUrl)已由父类填充
model.addAttribute("customData", "your custom value");
}
@Override
public int getPriority() {
// 返回优先级,数字越小优先级越高
// 如果多个策略支持同一文件类型,优先级高的会被选中
return 10;
}
}步骤 2:创建预览模板
在 src/main/resources/templates/preview/ 目录下创建预览模板文件:
your-template.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${fileName}</title>
<style>
body {
margin: 0;
padding: 20px;
background: #f5f5f5;
}
.preview-container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* 你的样式 */
</style>
</head>
<body>
<div class="preview-container">
<h2>${fileName}</h2>
<p>文件大小: ${fileSize} 字节</p>
<p>文件类型: ${fileType}</p>
<!-- 你的预览内容 -->
<div class="preview-content">
<!-- 使用 streamUrl 加载文件流 -->
<iframe src="${streamUrl}" width="100%" height="600px"></iframe>
</div>
</div>
</body>
</html>注意:模板中可以使用 Thymeleaf 语法,
PreviewContext中的数据会自动填充到模板中。
步骤 3:实现转换器(可选)
如果预览需要格式转换(如 Office 转 PDF),需要实现 IConverter 接口:
package com.xddcodec.fs.framework.preview.converter.impl;
import com.xddcodec.fs.framework.preview.converter.IConverter;
import lombok.extern.slf4j.Slf4j;
import java.io.InputStream;
@Slf4j
public class YourFileConverter implements IConverter {
@Override
public InputStream convert(InputStream sourceStream, String sourceExtension) {
try {
// 实现文件转换逻辑
// 例如:将 sourceExtension 格式转换为目标格式
log.info("开始转换文件: {} -> {}", sourceExtension, getTargetExtension());
// 执行转换...
// InputStream convertedStream = ...;
return convertedStream;
} catch (Exception e) {
log.error("文件转换失败: {}", e.getMessage(), e);
throw new RuntimeException("文件转换失败: " + e.getMessage(), e);
}
}
@Override
public String getTargetExtension() {
// 返回转换后的文件扩展名
return "pdf"; // 例如转换为 PDF
}
}步骤 4:在策略中使用转换器
如果策略需要使用转换器,重写 getConverter() 方法:
@Slf4j
@Component
@RequiredArgsConstructor
public class YourFilePreviewStrategy extends AbstractPreviewStrategy {
private final YourFileConverter converter;
@Override
public IConverter getConverter() {
return converter;
}
// 其他方法...
}步骤 5:配置转换器 Bean(如果需要)
如果转换器需要 Spring 管理,创建配置类:
package com.xddcodec.fs.framework.preview.config;
import com.xddcodec.fs.framework.preview.converter.impl.YourFileConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class YourConverterConfiguration {
@Bean
public YourFileConverter yourFileConverter() {
return new YourFileConverter();
}
}实现示例
示例 1:图片预览策略(简单预览)
package com.xddcodec.fs.framework.preview.strategy.impl;
import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
import com.xddcodec.fs.framework.preview.core.PreviewContext;
import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
@Slf4j
@Component
public class ImagePreviewStrategy extends AbstractPreviewStrategy {
@Override
public boolean support(FileTypeEnum fileType) {
return fileType == FileTypeEnum.IMAGE;
}
@Override
public String getTemplatePath() {
return "preview/image";
}
@Override
protected void fillSpecificModel(PreviewContext context, Model model) {
// 图片预览不需要额外数据
}
@Override
public int getPriority() {
return 3;
}
}示例 2:代码预览策略(带自定义数据)
package com.xddcodec.fs.framework.preview.strategy.impl;
import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
import com.xddcodec.fs.framework.preview.core.PreviewContext;
import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import java.util.Map;
@Slf4j
@Component
public class CodePreviewStrategy extends AbstractPreviewStrategy {
private static final Map<String, String> LANGUAGE_MAP = Map.ofEntries(
Map.entry("java", "java"),
Map.entry("js", "javascript"),
Map.entry("py", "python"),
// ... 更多映射
);
@Override
public boolean support(FileTypeEnum fileType) {
return fileType == FileTypeEnum.CODE;
}
@Override
public String getTemplatePath() {
return "preview/code";
}
@Override
protected void fillSpecificModel(PreviewContext context, Model model) {
// 根据文件扩展名确定编程语言
String language = LANGUAGE_MAP.getOrDefault(
context.getExtension() == null ? "" : context.getExtension().toLowerCase(),
"plaintext"
);
model.addAttribute("language", language);
}
@Override
public int getPriority() {
return 2;
}
}示例 3:Office 预览策略(需要转换)
package com.xddcodec.fs.framework.preview.strategy.impl;
import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
import com.xddcodec.fs.framework.preview.converter.IConverter;
import com.xddcodec.fs.framework.preview.converter.impl.OfficeToPdfConverter;
import com.xddcodec.fs.framework.preview.core.PreviewContext;
import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ui.Model;
@Slf4j
@RequiredArgsConstructor
public class OfficePreviewStrategy extends AbstractPreviewStrategy {
private final OfficeToPdfConverter officeToPdfConverter;
@Override
public boolean support(FileTypeEnum fileType) {
return fileType == FileTypeEnum.WORD || fileType == FileTypeEnum.PPT;
}
@Override
public String getTemplatePath() {
return "preview/pdf"; // Office 转 PDF 后使用 PDF 预览模板
}
@Override
public IConverter getConverter() {
return officeToPdfConverter;
}
@Override
protected void fillSpecificModel(PreviewContext context, Model model) {
// Office 转 PDF 后不需要额外数据
}
@Override
public int getPriority() {
return 5;
}
}高级功能
支持 Range 请求
对于大文件预览,可以实现 Range 请求支持,允许浏览器分段加载文件:
@Override
public boolean supportRange() {
// 如果不需要转换,通常支持 Range 请求
return getConverter() == null;
}注意:如果使用了转换器,通常不支持 Range 请求,因为需要先完成转换才能返回结果。
自定义响应扩展名
如果转换后文件扩展名发生变化,可以重写此方法:
@Override
public String getResponseExtension(String originalExtension) {
IConverter converter = getConverter();
if (converter != null) {
return converter.getTargetExtension();
}
return originalExtension;
}配置说明
预览基础配置
在 application.yml 中配置预览相关参数:
fs:
preview:
# 预览文件流处理 API
stream-api: http://localhost:8080/api/file/stream/preview
# 预览文件最大大小(字节),默认 500MB
max-file-size: 524288000
# 单次 Range 请求最大大小(字节),默认 10MB
max-range-size: 10485760
# 小文件直接传输阈值(字节),默认 10MB
small-file-size: 10485760
# 缓冲区大小(字节),默认 8KB
buffer-size: 8192Office 转 PDF 配置(可选)
如果需要 Office 文档预览功能,需要配置 LibreOffice:
fs:
preview:
office:
# 是否启用 Office 转换
enabled: true
# LibreOffice 安装路径
# Linux: /usr/lib/libreoffice
# Windows: C:/Program Files/LibreOffice
# Mac: /Applications/LibreOffice.app/Contents
office-home: C:/Program Files/LibreOffice
# 进程池大小
pool-size: 2
# 任务执行超时(毫秒)
task-execution-timeout: 120000
# 任务队列超时(毫秒)
task-queue-timeout: 30000
# 最大任务数
max-tasks-per-process: 200
# 转换缓存目录
cache-path: /tmp/office-convert注意:使用 Office 预览功能需要先安装 LibreOffice。详细安装和配置说明请参考环境准备 - LibreOffice 安装与配置。
注意事项
-
策略优先级:如果多个策略支持同一文件类型,系统会选择优先级最高的(
getPriority()返回值最小的)。确保为你的策略设置合适的优先级。 -
Spring 组件:预览策略必须标注
@Component注解,才能被 Spring 自动扫描和注入。 -
模板路径:
getTemplatePath()返回的路径是相对于templates目录的,不需要包含.html扩展名。 -
转换器线程安全:如果使用转换器,确保转换器实现是线程安全的,因为可能被多个请求并发调用。
-
资源清理:如果转换器使用了临时文件或其他资源,确保在转换完成后正确清理。
-
异常处理:转换过程中如果发生异常,应该抛出
RuntimeException或其子类,系统会统一处理。 -
性能考虑:文件转换通常是耗时操作,建议:
- 使用缓存机制(如转换结果缓存)
- 异步处理大文件转换
- 限制并发转换数量
测试预览策略
-
编译项目:确保策略类已正确编译。
-
启动应用:启动应用后,检查日志中是否有策略加载信息:
初始化预览策略管理器,已加载 X 个策略 -
验证策略注册:确认你的策略已被
PreviewStrategyManager加载。 -
测试预览:上传对应类型的文件,访问预览页面,验证预览功能是否正常。
-
测试转换:如果使用了转换器,测试转换功能是否正常工作。
常见问题
Q: 策略没有被加载?
A: 检查:
- 策略类是否标注了
@Component注解 - 策略类是否在 Spring 扫描路径内
- 查看启动日志确认策略管理器初始化信息
Q: 预览页面显示不正确?
A: 检查:
- 模板路径是否正确
- 模板中使用的变量是否在
fillSpecificModel中设置 - 浏览器控制台是否有 JavaScript 错误
Q: 转换失败?
A: 检查:
- 转换器配置是否正确
- 转换所需的依赖是否已安装(如 LibreOffice)
- 查看应用日志中的错误信息
- 临时文件目录是否有写入权限
Q: 如何支持新的文件类型?
A: 需要:
- 在
FileTypeEnum中添加新的文件类型枚举(如果不存在) - 创建对应的预览策略实现
- 创建预览模板
- 如果需要转换,实现对应的转换器