Free FS LogoFree FS
服务端

文件预览

详细介绍如何配置和开发 Free FS 的文件预览插件。

架构概述

Free FS 采用策略模式的插件化预览架构,支持通过 Spring 依赖注入机制动态加载预览策略。系统默认实现了多种文件类型的预览策略(图片、PDF、Office、代码、Markdown、音视频等),每种文件类型都有对应的预览策略实现。

核心组件

  • PreviewStrategy: 预览策略接口,定义了所有预览策略必须实现的方法
  • AbstractPreviewStrategy: 抽象预览策略,提供通用功能和默认实现
  • PreviewStrategyManager: 预览策略管理器,负责管理和选择预览策略
  • IConverter: 文件转换器接口,用于文件格式转换(如 Office 转 PDF)
  • PreviewContext: 预览上下文,封装预览所需的信息
  • FilePreviewConfig: 预览配置类,管理预览相关配置

策略加载流程

  1. 系统启动时,Spring 自动扫描所有标注了 @ComponentPreviewStrategy 实现类
  2. 所有策略被注入到 PreviewStrategyManager
  3. 策略按优先级(getPriority())排序,数字越小优先级越高
  4. 当需要预览文件时,管理器根据文件类型选择第一个支持该类型的策略
  5. 如果所有策略都不支持,则使用 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: 8192

Office 转 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 安装与配置

注意事项

  1. 策略优先级:如果多个策略支持同一文件类型,系统会选择优先级最高的(getPriority() 返回值最小的)。确保为你的策略设置合适的优先级。

  2. Spring 组件:预览策略必须标注 @Component 注解,才能被 Spring 自动扫描和注入。

  3. 模板路径getTemplatePath() 返回的路径是相对于 templates 目录的,不需要包含 .html 扩展名。

  4. 转换器线程安全:如果使用转换器,确保转换器实现是线程安全的,因为可能被多个请求并发调用。

  5. 资源清理:如果转换器使用了临时文件或其他资源,确保在转换完成后正确清理。

  6. 异常处理:转换过程中如果发生异常,应该抛出 RuntimeException 或其子类,系统会统一处理。

  7. 性能考虑:文件转换通常是耗时操作,建议:

    • 使用缓存机制(如转换结果缓存)
    • 异步处理大文件转换
    • 限制并发转换数量

测试预览策略

  1. 编译项目:确保策略类已正确编译。

  2. 启动应用:启动应用后,检查日志中是否有策略加载信息:

    初始化预览策略管理器,已加载 X 个策略
  3. 验证策略注册:确认你的策略已被 PreviewStrategyManager 加载。

  4. 测试预览:上传对应类型的文件,访问预览页面,验证预览功能是否正常。

  5. 测试转换:如果使用了转换器,测试转换功能是否正常工作。

常见问题

Q: 策略没有被加载?

A: 检查:

  • 策略类是否标注了 @Component 注解
  • 策略类是否在 Spring 扫描路径内
  • 查看启动日志确认策略管理器初始化信息

Q: 预览页面显示不正确?

A: 检查:

  • 模板路径是否正确
  • 模板中使用的变量是否在 fillSpecificModel 中设置
  • 浏览器控制台是否有 JavaScript 错误

Q: 转换失败?

A: 检查:

  • 转换器配置是否正确
  • 转换所需的依赖是否已安装(如 LibreOffice)
  • 查看应用日志中的错误信息
  • 临时文件目录是否有写入权限

Q: 如何支持新的文件类型?

A: 需要:

  1. FileTypeEnum 中添加新的文件类型枚举(如果不存在)
  2. 创建对应的预览策略实现
  3. 创建预览模板
  4. 如果需要转换,实现对应的转换器

目录