LOADING

加载过慢请开启缓存 浏览器默认开启

Minio前端直连,上传的文件无法预览而是直接下载

1 问题描述

正常情况下,我希望能在前端使用pdfjs等工具预览pdf或者使用其他工具预览图片,但是当使用预签名方式上传的文件,发现前端无论如何强制操作,浏览器都会直接下载这个文件,而不是预览。

2 原因

主要原因为,使用预签名上传的文件,上传过程中没有向后端上传一样给Minio服务器添加一个文件类型的描述。这样,文件的“Content-type”这一栏就不正确,而浏览器又是根据”Content-type”这个响应头来判断如何处理文件的,所以就会造成直接下载而无法预览的问题。

3 解决方法

解决方法也很简单,只需要在前端上传的时候设置响应头中的Content-type为需要的MIME类型就行了

axios.put(preSignedInfo.uploadUrl, options.file, {headers: {'Content-Type': options.file.type}})

这样,文件的Content-type就正常了,也就可以正常预览文件了。

4 补充

4.1 Minio预签名上传的两种方式:getPresignedObjectUrl方法

Minio 获取预签名上传经常被用到的方式为getPresignedObjectUrl,这个方法的教程如下
在Minio以STS方式获得临时凭据上传文件

我使用的也是这种方法,但是这种方法会造成前端上传的文件无法正常预览,我在getPresignedObjectUrl的入参里面没有找到有关设置文件类型的参数,所以在前端手动设置响应头

4.2 Minio预签名上传的两种方式:getPresignedPostFormData方法

getPresignedPostFormData方法的教程(见第5点)

另一种方法是getPresignedPostFormData方法,这种方法很少见有人用,但是它的方法中是可以设置文件类型的,而且这种方法非常严格,预签名和前端响应头只要有一点对不上,就会报错。

4.3 后端代码

public void testGetPresignedPostFormData() {
        try {
            // 为存储桶创建一个上传策略,过期时间为7天
            PostPolicy policy = new PostPolicy(BUCKET, ZonedDateTime.now().plusDays(7));
            // 设置一个参数key-value,值为上传对象的名称(保存在桶中的名字)
            policy.addEqualsCondition("key", "111.pdf");
            // 这种方法是预先定好了对象文件类型的
            // 添加 Content-Type以"image/"开头,表示只能上传照片
            policy.addStartsWithCondition("Content-Type", "application/pdf");
            // 设置上传文件的大小 10kiB to 10MiB.
            policy.addContentLengthRangeCondition(10 * 1024, 10 * 1024 * 1024);

            // 获得一个 可过期的 URL
            int durationSeconds = 3600;//秒
            //创建签名对象
            AssumeRoleProvider provider = new AssumeRoleProvider(
                    ENDPOINT,
                    ACCESS_KEY_COMPANY,
                    SECRET_KEY_COMPANY,
                    durationSeconds,//默认3600秒失效,设置小于这个就是3600,大于3600就实际值
                    POLICY_GET_AND_PUT,
                    REGION,
                    ROLE_ARN,
                    ROLE_SESSION_NAME,
                    null,
                    null);
            Credentials credentials = provider.fetch();
            StaticProvider staticProvider =
                    new StaticProvider(credentials.accessKey(), credentials.secretKey(), credentials.sessionToken());

            MinioClient minioClient = MinioClient.builder().endpoint(ENDPOINT)
                    .credentialsProvider(staticProvider).build();

            // 2. 获取策略的 认证令牌、签名等信息
            Map<String, String> formData = minioClient.getPresignedPostFormData(policy);

            // 3.模拟第三方,使用 OkHttp调用 Post上传对象
            // 创建 MultipartBody对象
            MultipartBody.Builder multipartBuilder = new MultipartBody.Builder();
            multipartBuilder.setType(MultipartBody.FORM);
            for (Map.Entry<String, String> entry : formData.entrySet()) {
                multipartBuilder.addFormDataPart(entry.getKey(), entry.getValue());
            }
            multipartBuilder.addFormDataPart("key", "111.pdf");// 必须要和策略参数一样
            multipartBuilder.addFormDataPart("Content-Type", "application/pdf");
            File uploadFile = new File("流程图.pdf"); // 这里的文件名不一样都没关系,只需要保证key一样就行
            // 上传文件的 fileName自定义,这里方便就用 objectName
            multipartBuilder.addFormDataPart(
                    "file", "流程图.pdf", RequestBody.create(uploadFile, null));
            // 使用OkHttp调用Post上传对象
            Request request =
                    new Request.Builder()
                            .url("http://localhost:9000/" + BUCKET)
                            .post(multipartBuilder.build())
                            .build();

            OkHttpClient httpClient = new OkHttpClient().newBuilder().build();

            Response response = httpClient.newCall(request).execute();
            if (response.isSuccessful()) {
                System.out.println("uploaded successfully using POST object");
            } else {
                System.out.println("Failed to upload Pictures");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public void testGetPresignedObjectUrl() {
        try {
            // 获得一个 可过期的 URL
            int durationSeconds = 3600;//秒
            //创建签名对象
            AssumeRoleProvider provider = new AssumeRoleProvider(
                    ENDPOINT,
                    ACCESS_KEY_COMPANY,
                    SECRET_KEY_COMPANY,
                    durationSeconds,//默认3600秒失效,设置小于这个就是3600,大于3600就实际值
                    POLICY_GET_AND_PUT,
                    REGION,
                    ROLE_ARN,
                    ROLE_SESSION_NAME,
                    null,
                    null);

            Credentials credentials = provider.fetch();
            StaticProvider staticProvider =
                    new StaticProvider(credentials.accessKey(), credentials.secretKey(), credentials.sessionToken());

            MinioClient minioClient = MinioClient.builder().endpoint(ENDPOINT)
                    .credentialsProvider(staticProvider).build();
            Map<String, String> reqParams = new HashMap<>();
            reqParams.put("response-content-type", "application/json");
            String url = minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.PUT)//这里必须是PUT,如果是GET的话就是文件访问地址了。如果是POST上传会报错.
                            .bucket(BUCKET)
                            .object("test.pdf")
                            .expiry(60 * 30) // 30分钟
                            .extraQueryParams(reqParams)
                            .build());
            System.out.println(url); // 前端直传需要的url地址
            // TODO 把这个url复制到前端,然后调用前端上传接口即可。
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

4.4 前端代码

前端使用的是Element-plus 中的upload组件,下面是上传逻辑

  const httpRequest = async (options: UploadRequestOptions) => {
    // 1.1 生成文件名称 默认
    const fileName = options.file.name
    // 1.2 获取文件预签名地址
    const uploadUrl = "把后端得到的预签名地址复制到这里"
    // 1.4 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持)
    return axios.put(uploadUrl, options.file, {headers: {'Content-Type': options.file.type}})
}