问题根源:图片url上传时,为什么前端能显示,而COS对象存储却不能预览

一. 问题分析

  1. 前端浏览器如何工作: 当浏览器通过URL请求一张图片时,COS服务器在返回图片数据的同时,会在HTTP响应头(HTTP Header)中包含一个叫做 Content-Type 的字段。例如,对于一张JPEG图片,这个头信息会是 Content-Type: image/jpeg。浏览器看到这个头信息,就知道“这是一个JPEG图片”,然后就会正确地将其渲染出来。因此,只要 Content-Type 是正确的,即使URL末尾没有 .jpg 后缀,浏览器也能正常显示

  2. COS控制台如何工作: COS的网页控制台是一个管理工具,它为了快速、简单地展示文件列表和缩略图,通常会采取一种“偷懒”的策略:直接通过对象名称(文件名)的后缀来判断文件类型。当它看到一个像 ..._WBjlp6PwStfS1Rc0 这样没有后缀的文件名时,它会认为这是一个未知的二进制文件,因此不会尝试去生成预览图。

二.解决方案

第1步:添加 Apache Tika 依赖

<!-- pom.xml -->
<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-core</artifactId>
    <version>2.9.1</version> <!-- 建议使用最新的稳定版本 -->
</dependency>

第2步:创建文件后缀工具类

package com.yupi.yupicturebackend.exception;
​
import cn.hutool.core.util.StrUtil;
import org.apache.tika.mime.MimeType;
import org.apache.tika.mime.MimeTypeException;
import org.apache.tika.mime.MimeTypes;
​
/**
 * 文件后缀工具类
 */
public class FileExtensionUtils {
​
    /**
     * 根据MIME类型获取标准的文件后缀名
     * @param mimeType 例如 "image/jpeg"
     * @return 例如 ".jpg"
     */
    public static String getExtensionFromMimeType(String mimeType) {
        if (StrUtil.isBlank(mimeType)) {
            return "";
        }
        try {
            MimeTypes allTypes = MimeTypes.getDefaultMimeTypes();
            MimeType type = allTypes.forName(mimeType);
            String extension = type.getExtension();
​
            // 将 .jpeg 这种官方但不太常用的后缀,统一为 .jpg
            if (".jpeg".equals(extension)) {
                return ".jpg";
            }
            return extension;
​
        } catch (MimeTypeException e) {
            // 如果MIME类型无法识别,可以记录一个警告日志
            return "";
        }
    }
}
​

第3步:修改 UrlPictureUpload.java

package com.yupi.yupicturebackend.manager.upload;
​
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.Method;
import com.yupi.yupicturebackend.exception.BusinessException;
import com.yupi.yupicturebackend.exception.ErrorCode;
import com.yupi.yupicturebackend.exception.ThrowUtils;
import com.yupi.yupicturebackend.exception.FileExtensionUtils;
import org.springframework.stereotype.Service;
​
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
​
/**
 * URL 图片上传
 */
@Service
public class UrlPictureUpload extends PictureUploadTemplate {
​
    // 【新增】创建一个ThreadLocal来在线程内传递 Content-Type
    private static final ThreadLocal<String> contentTypeThreadLocal = new ThreadLocal<>();
​
    @Override
    protected void validPicture(Object inputSource) {
        // 在每次校验开始前,清理一下ThreadLocal,防止上次失败的请求数据残留
        contentTypeThreadLocal.remove();
​
        String fileUrl = (String) inputSource;
        // 1. 校验非空
        ThrowUtils.throwIf(StrUtil.isBlank(fileUrl), ErrorCode.PARAMS_ERROR, "文件地址为空");
​
        // 2. 校验 URL 格式
        try {
            new URL(fileUrl);
        } catch (MalformedURLException e) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件地址格式不正确");
        }
        // 3. 校验 URL 的协议
        ThrowUtils.throwIf(!fileUrl.startsWith("http://") && !fileUrl.startsWith("https://"),
                ErrorCode.PARAMS_ERROR, "仅支持 HTTP 或 HTTPS 协议的文件地址"
        );
        // 4. 发送 HEAD 请求验证文件是否存在
        HttpResponse httpResponse = null;
        try {
            httpResponse = HttpUtil.createRequest(Method.HEAD, fileUrl)
                    .execute();
            // 未正常返回,无需执行其他判断
            if (httpResponse.getStatus() != HttpStatus.HTTP_OK) {
                // 如果文件不存在或无法访问,这里直接返回即可,模板的后续流程会处理
                return;
            }
            // 5. 文件存在,文件类型校验
            String contentType = httpResponse.header("Content-Type");
            // 获取到contentType后,存入ThreadLocal
            if (StrUtil.isNotBlank(contentType)) {
                contentTypeThreadLocal.set(contentType);
            }
​
            // 不为空,才校验是否合法,这样校验规则相对宽松
            if (StrUtil.isNotBlank(contentType)) {
                // 允许的图片类型
                final List<String> ALLOW_CONTENT_TYPES = Arrays.asList("image/jpeg", "image/jpg", "image/png", "image/webp");
                ThrowUtils.throwIf(!ALLOW_CONTENT_TYPES.contains(contentType.toLowerCase()),
                        ErrorCode.PARAMS_ERROR, "文件类型错误");
            }
            // 6. 文件存在,文件大小校验
            String contentLengthStr = httpResponse.header("Content-Length");
            if (StrUtil.isNotBlank(contentLengthStr)) {
                try {
                    long contentLength = Long.parseLong(contentLengthStr);
                    final long ONE_M = 1024 * 1024;
                    ThrowUtils.throwIf(contentLength > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2MB");
                } catch (NumberFormatException e) {
                    throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件大小格式异常");
                }
            }
        } finally {
            // 记得释放资源
            if (httpResponse != null) {
                httpResponse.close();
            }
        }
    }
​
    @Override
    protected String getOriginFilename(Object inputSource) {
        String fileUrl = (String) inputSource;
        // 从 ThreadLocal 中获取 Content-Type
        String contentType = contentTypeThreadLocal.get();
​
        // 无论是否获取到,用完后立即清理 ThreadLocal,防止内存泄漏和线程复用问题
        contentTypeThreadLocal.remove();
​
        // 如果在 validPicture 阶段没有获取到 Content-Type,则沿用旧逻辑
        if (StrUtil.isBlank(contentType)) {
            return FileUtil.getName(fileUrl);
        }
​
        // 使用工具类将 content-type 转换为后缀
        String extension = FileExtensionUtils.getExtensionFromMimeType(contentType);
​
        // 获取URL中的原始文件名 (例如 "cat.jpg" 或 "cat")
        String originalFilename = FileUtil.getName(fileUrl);
        // 获取不带后缀的主文件名 (例如 "cat")
        String mainName = FileUtil.mainName(originalFilename);
​
        // 如果无法转换得到后缀,或者原始文件名已经有正确的后缀,则直接返回原始文件名
        if (StrUtil.isBlank(extension) || originalFilename.toLowerCase().endsWith(extension)) {
            return originalFilename;
        }
​
        // 否则,为主文件名拼接上正确的后缀
        return mainName + extension;
    }
​
    @Override
    protected void processFile(Object inputSource, File file) throws Exception {
        String fileUrl = (String) inputSource;
        // 下载文件到临时目录 (此方法无需改动)
        HttpUtil.downloadFile(fileUrl, file);
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值