Hutool、Forest附件上传xlsx类型文件异常

文章描述了一个项目中遇到的问题,即上传Excel文件时因MIME类型缺失导致对方系统无法识别。通过源码分析和定位,发现问题出在hutool和Forest框架使用的JDKcontentType映射文件中未包含对Office文件的映射。解决方案是添加xlsxMIME类型到该文件后,重启服务解决了问题。

一、问题背景

        最近参与某个项目对接(内容主要为我方系统提供统一界面提交数据,后端转发数据至其他系统处理),客户线上发现一个问题,就是上传了excel附件类型后,对方项目系统中迟迟未做响应,经过排查后发现在提交数据时因为附件上传失败导致后续整体调用失败(业务需要保证所有数据都成功)。

错误日志如下:

2023-11-14 16:56:40.579 -- [TID:Ignored_Trace] [http-nio-8030-exec-5] ERROR c.d.i.service.xxService.uploadFile 上传文件异常, 响应结果-{"msg":"文件类型不符, 限制上传filename: ","filename":""}

二、定位问题

        排查过程中发现,附件为图片、txt类型文件调用该接口可以正常上传,线上问题中的xlsx死活上传不了,遂与对方系统开发沟通,怀疑是上传时缺少后缀才会报,但从其他日志打印中可以明确的是这种情况并不存在,我开始怀疑是对方系统有问题,本地因对方系统白名单限制无法直接调用对方系统接口,因此想直接上服务器本地使用curl进行文件上传来验证这个问题。

curl -v -X POST -H 'token:w68sTP2t_5XeFLbPPPuJsCosadWlxbsmtl0xAtzsmhI' -F 'file=@1.xlsx' 'http://x.x.x.x/upload/'

结果依旧显示文件类型不符。

 

         将这个情况提供给对方后,但对方提供了一个点让我在  curl -F 中添加文件MIME类型再试下,居然上传成功了。

curl -v -X POST -H 'token:w68sTP2t_5XeFLbPPPuJsCosadWlxbsmtl0xAtzsmhI' -F 'file=@1.xlsx;type=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 'http://x.x.x.x/upload/'

        

        那到此基本可以肯定的是代码中也是因为缺少这个MIME类型或者MIME类型不匹配导致对方服务器无法正常识别文件。

三、源码分析

        首先找到问题代码位置,如下显示:

import cn.hutool.http.*;

HttpResponse response = HttpRequest.post(fileOpUploadUrl)
                                    .header("ts", ts).header("token", token)
                                    .form("file",file, originalFilename)
                                    .setReadTimeout(30000)
                                    .setConnectionTimeout(3000)
                                    .execute();

        继续往下追踪:

 封装Multipart:

 

发送文件对象,并在输出流中提供文件的MIME类型

 最终定位到这边,发现是调用底层的jdk中的包去根据文件名获取MIME类型:

 

 那么底层是如何获取文件名和MIME的映射关系的呢,继续往下看,找到底层 MimeTable 类

    public synchronized void load() {
        Properties var1 = new Properties();
        File var2 = null;

        try {
            String var4 = System.getProperty("content.types.user.table");
            if (var4 != null) {
                var2 = new File(var4);
                if (!var2.exists()) {
                    var2 = new File(System.getProperty("java.home") + File.separator + "lib" + File.separator + "content-types.properties");
                }
            } else {
                var2 = new File(System.getProperty("java.home") + File.separator + "lib" + File.separator + "content-types.properties");
            }

            BufferedInputStream var3 = new BufferedInputStream(new FileInputStream(var2));
            var1.load(var3);
            var3.close();
        } catch (IOException var5) {
            System.err.println("Warning: default mime table not found: " + var2.getPath());
            return;
        }

        this.parse(var1);
    }

 至此一目了然,只需要确定这个映射文件位置即可:

打开该文件,可以发现这个映射文件涵盖了大部分类型的文件后缀(这里只截取一部分,具体支持哪些有兴趣的可以自己去看):

        但对于office相关的文件并未做过映射,因此可以根据这个判断,不止xlsx类型上传有问题,其他的office类型文件上传时一样会有这个问题。

PS:考虑到hutool有问题,联想到目前其他项目中使用的 Forest 框架是不是同样具有问题,继续查看Forest源码

往下查看2种executor如何构建附件body:

 最终进入一个共同的抽象类 AbstractBodyBuilder 类的  buildBody 方法中

进入各自实现类中,最终都会定位到相同的代码去获取MIME类型

总结:hutool和Forest工具类,在上传附件时,文件的MIME类型都是通过jdk自带的contentType的映射文件来获取的

四、解决方案

        在jdk路径下的content-types.properties中按照其他文件的映射格式,将需要补充的类型添加上去后,重启服务即可解决。

# 添加xlsx类型
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; \
    description=XLSX document;\
    file_extensions=.xlsx

<think> 我们被要求使用Python的Flask框架中的send_file功能来返回一个xlsx类型文件。根据引用[2]和引用[4],我们知道在Flask中可以使用`send_from_directory`或`send_file`函数来实现文件下载。这里我们需要返回的是xlsx文件,这是一种二进制文件。 步骤: 1. 确保已经生成了xlsx文件,或者有一个xlsx文件可供发送。 2. 在Flask路由中,使用`send_file`函数发送该文件。 注意:由于xlsx文件是二进制文件,我们需要确保以二进制模式读取,但`send_file`函数内部会处理。 引用[2]中提到了两种方法: - 方法二:使用`send_from_directory`函数,适合从指定目录发送文件。 - 方法三:使用`send_file`函数,并可以配合X-Sendfile头(需要服务器支持)。 这里我们直接使用`send_file`函数,因为它更通用。 示例代码: 假设我们有一个生成xlsx文件的函数,或者我们已经有一个xlsx文件位于服务器的某个路径。 情况一:发送已存在的xlsx文件 例如,我们有一个名为`report.xlsx`的文件存放在`/var/www/reports`目录下。 情况二:动态生成xlsx文件并发送 我们可以使用如`openpyxl`(引用[1])等库动态创建xlsx文件,然后将其发送。但是注意,动态生成的文件我们可能不想保存在服务器上,可以使用`BytesIO`来创建一个内存中的文件并发送。 我们将分别展示这两种情况。 首先,确保安装了必要的库: - Flask - openpyxl(用于生成xlsx文件) 安装命令: pip install flask openpyxl 示例1:发送已存在的xlsx文件 ```python from flask import Flask, send_file app = Flask(__name__) @app.route('/download/existing') def download_existing(): # 假设文件路径 file_path = '/var/www/reports/report.xlsx' # 发送文件,设置下载时的文件名(可选),as_attachment=True表示作为附件下载 return send_file(file_path, as_attachment=True, download_name='report.xlsx') ``` 示例2:动态生成xlsx文件并发送(使用内存,不保存到磁盘) ```python from flask import Flask, send_file from openpyxl import Workbook from io import BytesIO app = Flask(__name__) @app.route('/download/dynamic') def download_dynamic(): # 创建一个工作簿 wb = Workbook() ws = wb.active ws.title = "Report" # 在单元格中写入数据 ws['A1'] = 'Hello' ws['B1'] = 'World' # 创建一个字节流对象 output = BytesIO() # 将工作簿保存到字节流 wb.save(output) # 将字节流的指针移动到开头 output.seek(0) # 发送文件 return send_file( output, as_attachment=True, download_name='dynamic_report.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) ``` 注意: - 在动态生成的情况下,我们使用`BytesIO`来在内存中创建文件,避免磁盘I/O。 - 我们指定了MIME类型xlsx文件的正确MIME类型:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`。这样浏览器可以正确识别文件类型。 - `as_attachment=True`会使得浏览器以附件形式下载,而不是尝试在浏览器中打开(如果浏览器支持的话)。 - `download_name`参数指定了下载时建议的文件名。 另外,引用[4]中提到了文件上传下载的实现,我们这里只关注下载(返回)xlsx文件。 安全性: - 如果文件名是由用户提供的,务必使用`secure_filename`(如引用[2]中提到的)来清理文件名,防止目录遍历攻击。 例如,如果我们要发送的文件名来自用户输入: ```python from flask import send_from_directory, request from werkzeug.utils import secure_filename @app.route('/download/user_file') def download_user_file(): filename = request.args.get('filename') safe_filename = secure_filename(filename) directory = '/var/www/reports' return send_from_directory(directory, safe_filename, as_attachment=True) ``` 但是注意,我们这里要求的是使用`send_file`,所以用`send_file`也可以实现,但需要拼接路径,并且要确保路径安全。推荐使用`send_from_directory`,因为它更安全,可以避免路径遍历。 不过,如果一定要用`send_file`发送用户请求的文件(注意安全): ```python import os from flask import send_file, request from werkzeug.utils import secure_filename @app.route('/download/user_file') def download_user_file(): filename = request.args.get('filename') safe_filename = secure_filename(filename) directory = '/var/www/reports' # 拼接路径,确保安全 file_path = os.path.join(directory, safe_filename) # 检查文件是否存在等(这里省略) return send_file(file_path, as_attachment=True, download_name=safe_filename) ``` 总结:在Flask中返回xlsx文件,主要使用`send_file`函数,对于动态生成的内容,使用`BytesIO`;对于静态文件,直接指定路径。注意设置正确的MIME类型和安全性。 相关问题: 1. 如何在Flask中安全地提供用户上传文件下载? 2. 如何使用Flask发送其他类型的二进制文件(如PDF)? 3. 动态生成大型xlsx文件时,如何优化内存使用? </think> ### Python使用`send_file`返回xlsx文件的方法 在Flask框架中,可以使用`send_file`函数高效地返回xlsx格式的Excel文件,以下是具体实现方法和完整示例: --- #### 方法一:直接发送已存在的xlsx文件(最简单) ```python from flask import Flask, send_file app = Flask(__name__) @app.route('/download/report') def download_report(): file_path = 'path/to/your/report.xlsx' # 替换为实际路径 return send_file( file_path, as_attachment=True, # 强制下载 download_name='月度报表.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) ``` **关键参数说明**: - `as_attachment=True`:浏览器强制下载而非预览 - `download_name`:用户下载时显示的文件名 - `mimetype`:xlsx文件的正确MIME类型[^1] --- #### 方法二:动态生成xlsx文件后发送(结合openpyxl) ```python from flask import Flask, send_file from openpyxl import Workbook from io import BytesIO app = Flask(__name__) @app.route('/generate/report') def generate_report(): # 1. 动态创建工作簿 wb = Workbook() ws = wb.active ws.title = "销售数据" # 2. 填充Excel数据 ws['A1'] = "产品名称" ws['B1'] = "销售量" ws.append(["iPhone", 1200]) ws.append(["iPad", 800]) # 3. 保存到内存缓冲区 buffer = BytesIO() wb.save(buffer) buffer.seek(0) # 重置指针位置 # 4. 发送文件 return send_file( buffer, as_attachment=True, download_name='动态报表.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) ``` **优势**: - 无需物理存储临时文件 - 使用内存缓冲区(`BytesIO`)减少磁盘I/O开销 - 支持实时数据生成[^1] --- #### 安全注意事项 1. **文件名过滤**:使用`secure_filename`防止路径遍历攻击 ```python from werkzeug.utils import secure_filename filename = secure_filename(request.args.get('file')) # 清理用户输入 ``` 2. **路径验证**:禁止用户指定绝对路径 ```python import os base_dir = '/safe/directory' file_path = os.path.join(base_dir, filename) # 限定安全目录 ``` --- #### 性能优化建议 1. **大文件处理**:对超过100MB的文件启用分块传输 ```python return send_file(file_path, conditional=True) # 支持Range请求 ``` 2. **缓存策略**:添加缓存头减少重复生成 ```python response = send_file(...) response.headers['Cache-Control'] = 'max-age=3600' return response ``` --- #### 完整工作流程示例 ```mermaid graph TD A[用户请求/download] --> B{文件类型} B -->|静态文件| C[读取磁盘xlsx文件] B -->|动态生成| D[用openpyxl创建工作簿] D --> E[填充数据] C & E --> F[写入内存缓冲区] F --> G[发送send_file响应] G --> H[浏览器下载xlsx] ``` 通过上述方法,您可以安全高效地在Flask应用中实现xlsx文件的生成和下载功能。实际部署时建议结合Nginx的`X-Accel-Redirect`实现零拷贝传输[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值