getFileNameFromDisposition更加规范,从多个头的值中获取,且filename*优先级更高

This commit is contained in:
Looly 2024-05-20 09:49:26 +08:00
parent 619d5ca61c
commit 49c891cb50
3 changed files with 127 additions and 32 deletions

View File

@ -19,6 +19,7 @@ import org.dromara.hutool.http.HttpException;
import org.dromara.hutool.http.client.body.ResponseBody;
import org.dromara.hutool.http.meta.ContentTypeUtil;
import org.dromara.hutool.http.meta.HeaderName;
import org.dromara.hutool.http.meta.HttpHeaderUtil;
import java.io.Closeable;
import java.io.IOException;
@ -92,7 +93,7 @@ public interface Response extends Closeable {
* @throws HttpException 包装IO异常
*/
default String bodyStr() throws HttpException {
try(final ResponseBody body = body()){
try (final ResponseBody body = body()) {
return body.getString();
} catch (final IOException e) {
throw new IORuntimeException(e);
@ -106,7 +107,7 @@ public interface Response extends Closeable {
* @return byte[]
*/
default byte[] bodyBytes() {
try(final ResponseBody body = body()){
try (final ResponseBody body = body()) {
return body.getBytes();
} catch (final IOException e) {
throw new IORuntimeException(e);
@ -136,6 +137,16 @@ public interface Response extends Closeable {
return header(name.toString());
}
/**
* 根据name获取对应的头信息列表
*
* @param name Header名
* @return Header值
*/
default List<String> headerList(final String name) {
return HttpHeaderUtil.headerList(headers(), name);
}
/**
* 获取内容编码
*
@ -172,8 +183,7 @@ public interface Response extends Closeable {
* @since 4.6.2
*/
default boolean isChunked() {
final String transferEncoding = header(HeaderName.TRANSFER_ENCODING);
return "Chunked".equalsIgnoreCase(transferEncoding);
return "Chunked".equalsIgnoreCase(header(HeaderName.TRANSFER_ENCODING));
}
/**
@ -186,6 +196,22 @@ public interface Response extends Closeable {
return header(HeaderName.SET_COOKIE);
}
/**
* 从Content-Disposition头中获取文件名以参数名为`filename`为例规则为
* <ul>
* <li>首先按照RFC5987规范检查`filename*`参数对应的值`filename*="example.txt"`则获取`example.txt`</li>
* <li>如果找不到`filename*`参数则检查`filename`参数对应的值`filename="example.txt"`则获取`example.txt`</li>
* </ul>
* 按照规范`Content-Disposition`可能返回多个此处遍历所有返回头并且`filename*`始终优先获取即使`filename`存在并更靠前<br>
* 参考https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
*
* @param paramName 文件参数名如果为{@code null}则使用默认的`filename`
* @return 文件名empty表示无
*/
default String getFileNameFromDisposition(final String paramName) {
return HttpHeaderUtil.getFileNameFromDisposition(headers(), paramName);
}
/**
* 链式处理结果
*

View File

@ -12,26 +12,20 @@
package org.dromara.hutool.http.client.body;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.io.StreamProgress;
import org.dromara.hutool.core.io.file.FileNameUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.io.stream.SyncInputStream;
import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.core.regex.ReUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.util.ObjUtil;
import org.dromara.hutool.http.HttpException;
import org.dromara.hutool.http.HttpGlobalConfig;
import org.dromara.hutool.http.client.Response;
import org.dromara.hutool.http.html.HtmlUtil;
import org.dromara.hutool.http.meta.HeaderName;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.*;
/**
* 响应体部分封装
@ -234,30 +228,11 @@ public class ResponseBody implements HttpBody, Closeable {
}
// 从头信息中获取文件名
final String fileName = getFileNameFromDisposition(ObjUtil.defaultIfNull(customParamName, "filename"));
final String fileName = response.getFileNameFromDisposition(customParamName);
if (StrUtil.isBlank(fileName)) {
throw new HttpException("Can`t get file name from [Content-Disposition]!");
}
return FileUtil.file(targetFileOrDir, fileName);
}
/**
* 从Content-Disposition头中获取文件名
*
* @param paramName 文件名的参数名
* @return 文件名empty表示无
* @since 5.8.10
*/
private String getFileNameFromDisposition(final String paramName) {
String fileName = null;
final String disposition = response.header(HeaderName.CONTENT_DISPOSITION);
if (StrUtil.isNotBlank(disposition)) {
fileName = ReUtil.get(paramName + "=\"(.*?)\"", disposition, 1);
if (StrUtil.isBlank(fileName)) {
fileName = StrUtil.subAfter(disposition, paramName + "=", true);
}
}
return fileName;
}
// endregion ---------------------------------------------------------------------------- Private Methods
}

View File

@ -0,0 +1,94 @@
/*
* Copyright (c) 2024. looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* https://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.http.meta;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.map.CaseInsensitiveMap;
import org.dromara.hutool.core.regex.ReUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.util.ObjUtil;
import java.util.List;
import java.util.Map;
/**
* HTTP头相关方法
*
* @author Looly
* @since 6.0.0
*/
public class HttpHeaderUtil {
/**
* 根据name获取对应的头信息列表
*
* @param headers 头列表
* @param name Header名
* @return Header值
*/
public static List<String> headerList(final Map<String, List<String>> headers, final String name) {
if (StrUtil.isBlank(name)) {
return null;
}
final CaseInsensitiveMap<String, List<String>> headersIgnoreCase = new CaseInsensitiveMap<>(headers);
return headersIgnoreCase.get(name.trim());
}
/**
* 从Content-Disposition头中获取文件名以参数名为`filename`为例规则为
* <ul>
* <li>首先按照RFC5987规范检查`filename*`参数对应的值`filename*="example.txt"`则获取`example.txt`</li>
* <li>如果找不到`filename*`参数则检查`filename`参数对应的值`filename="example.txt"`则获取`example.txt`</li>
* </ul>
* 按照规范`Content-Disposition`可能返回多个此处遍历所有返回头并且`filename*`始终优先获取即使`filename`存在并更靠前<br>
* 参考https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
*
* @param headers 头列表
* @param paramName 文件参数名如果为{@code null}则使用默认的`filename`
* @return 文件名empty表示无
*/
public static String getFileNameFromDisposition(final Map<String, List<String>> headers, String paramName) {
paramName = ObjUtil.defaultIfNull(paramName, "filename");
final List<String> dispositions = headerList(headers, HeaderName.CONTENT_DISPOSITION.name());
String fileName = null;
if (CollUtil.isNotEmpty(dispositions)) {
// filename* 采用了 RFC 5987 中规定的编码方式优先读取
fileName = getFileNameFromDispositions(dispositions, StrUtil.addSuffixIfNot(paramName, "*"));
if ((!StrUtil.endWith(fileName, "*")) && StrUtil.isBlank(fileName)) {
fileName = getFileNameFromDispositions(dispositions, paramName);
}
}
return fileName;
}
/**
* 从Content-Disposition头中获取文件名
*
* @param dispositions Content-Disposition头列表
* @param paramName 文件参数名
* @return 文件名empty表示无
*/
private static String getFileNameFromDispositions(final List<String> dispositions, String paramName) {
String fileName = null;
for (final String disposition : dispositions) {
fileName = ReUtil.getGroup1(paramName + "=\"(.*?)\"", disposition);
if (StrUtil.isNotBlank(fileName)) {
break;
}
}
return fileName;
}
}