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}})
}