使用Retrofit2将文件上传至AWS S3预签名URL

8

我正在尝试使用预签名URL将文件上传到亚马逊S3。我从服务器获取该URL,该服务器生成URL并将其作为JSON对象的一部分发送给我。我将URL作为字符串获取,类似于以下内容:

https://com-example-mysite.s3-us-east-1.amazonaws.com/userFolder/ImageName?X-Amz-Security-Token=xxfooxx%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fxxbarxx%3D&X-Amz-Algorithm=xxAlgoxx&X-Amz-Date=20170831T090152Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Credential=xxcredxx&X-Amz-Signature=xxsignxx

很不幸的是,当我将此传递给Retrofit2时,它会修改字符串并尝试将其转换为URL。我已经设置了encoding=true,这解决了大部分问题,但并非完全解决。我知道这个字符串本来是可以工作的。我在Postman中尝试过,并获得了成功的响应。
首先,我尝试将整个字符串(除了baseUrl之外剪切的部分)直接放入路径中。
public interface UpdateImageInterface {
    @PUT("{url}")
    Call<Void> updateImage(@Path(value="url", encoded=true) String url, Body RequestBody image);
}

调用代码:

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://com-example-mysite.s3-us-east-1.amazonaws.com/userFolder/")
            .build();

    UpdateImageInterface imageInterface = retrofit.create(UpdateImageInterface.class);
    // imageUrl is "ImageName..."
    Call<Void> call = imageInterface.updateImage(imageUrl, requestFile);

这个方法基本可行,但是“ImageName”后面的“?”会被转换为“%3F”,导致请求失败/400错误。
我接下来尝试使用Retrofit2创建查询,然后将整个包含多个查询的字符串转储到查询中。
public interface UpdateImageInterface {
    @PUT("ImageName")
    Call<Void> updateProfilePhoto(@Query(value="X-Amz-Security-Token", encoded = true) String token, @Body RequestBody image);
}

调用代码:

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://com-example-mysite.s3-us-east-1.amazonaws.com/userFolder/")
            .build();

    UpdateImageInterface imageInterface = retrofit.create(UpdateImageInterface.class);
    // imageUrl is "xxfooxx..."
    Call<Void> call = imageInterface.updateImage(imageUrl, requestFile);

这样可以正确呈现'?',但所有的'&'都会被改变为"%26"。
最后,我尝试将整个字符串传递给baseUrl(),但是由于结尾没有'/'而导致IllegalArgumentException报错。
我知道我可以解析预签名URL以进行多个查询并在Retrofit2中组装它们,因为应该使用查询,但我想避免这种处理。
重述问题如下:
有没有一种简单的方法(不需要大量字符串解析)使用Retrofit2和预签名URL上传文件到S3?

这个库或者它依赖的某些东西似乎有点问题。请看这里的评论:https://github.com/square/retrofit/issues/1199... 特别是最后一个评论。 - Michael - sqlbot
2个回答

16

通过同事的帮助,这是解决方案。

public interface UpdateImageInterface {
    @PUT
    Call<Void> updateImage(@Url String url, @Body RequestBody image);
}

调用代码:

    String CONTENT_IMAGE = "image/jpeg";

    File file = new File(localPhotoPath);    // create new file on device
    RequestBody requestFile = RequestBody.create(MediaType.parse(CONTENT_IMAGE), file);

    /* since the pre-signed URL from S3 contains a host, this dummy URL will
     * be replaced completely by the pre-signed URL.  (I'm using baseURl(String) here
     * but see baseUrl(okhttp3.HttpUrl) in Javadoc for how base URLs are handled
     */
    Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://www.dummy.com/")
        .build();

    UpdateImageInterface imageInterface = retrofit.create(UpdateImageInterface.class);
    // imageUrl is the String as received from AWS S3
    Call<Void> call = imageInterface.updateImage(imageUrl, requestFile);

有关@Url(类Url)和baseUrl()(类Retrofit.Builder)的信息,请参阅Javadoc

MediaTypeOkHttp库中的一个类,通常与Retrofit(来自Square)一起使用。有关传递给解析方法的常量的信息可以在Javadoc中找到。


1
@AmitBarjatya 编辑了回答以包括 requestFile 的声明。 - Gary99
@Gary99,CONTENT_IMAGE是什么? - Eric
1
@Eric 请查看更新后的答案。添加了CONTENT_IMAGE的定义和有关其来源的信息。 - Gary99
1
我在上传视频文件时收到了403错误,有什么想法吗? - charitha amarasinghe
1
403错误通常是由上传文件结构问题引起的。 - charitha amarasinghe
显示剩余6条评论

1

使用以下内容直接上传到S3,使用预签名URL。

@Multipart
@PUT
@Headers("x-amz-acl:public-read")
Call<Void> uploadFile(@Url String url, @Header("Content-Type") String contentType, @Part MultipartBody.Part part);

关于这个解决方案 @Headers("x-amz-acl:public-read") 的注意事项是,应该谨慎添加,因为它可能导致签名不匹配。 - Amit Barjatya

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接