带有 Android Retrofit V2 库的 AWS S3 Rest API,上传的图像已损坏

Posted

技术标签:

【中文标题】带有 Android Retrofit V2 库的 AWS S3 Rest API,上传的图像已损坏【英文标题】:AWS S3 Rest API with Android Retrofit V2 library, uploaded image is damaged 【发布时间】:2015-12-16 06:25:41 【问题描述】:

我正在尝试将 Image 从我的 Android APP 上传到 Amazon AWS S3,我需要使用 AWS Restful API

我正在使用Retrofit 2 提出请求。

我的应用程序与 Amazon S3 成功连接并按预期执行请求,但是当我尝试从 Bucket 查看Image 时,图片没有打开。我将Image 下载到我的电脑并尝试打开,但不断收到图像已损坏的消息。

下面看看我的完整代码。

我的 Gradle 依赖项

compile 'com.squareup.retrofit:retrofit:2.0.0-beta1'
compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1'
compile 'net.danlew:android.joda:2.8.2'

这里创建了一个文件并开始请求

File file = new File(mCurrentPhotoPath);
RequestBody body = RequestBody.create(MediaType.parse("image/jpeg"), file);
uploadImage(body, "photo_name.jpeg");

改造界面

public interface AwsS3 

    @Multipart
    @PUT("/Key")
    Call<String> upload(@Path("Key") String Key,
                @Header("Content-Length") long length,
                @Header("Accept") String accept,
                @Header("Host") String host,
                @Header("Date") String date,
                @Header("Content-type") String contentType,
                @Header("Authorization") String authorization,
                @Part("Body") RequestBody body);

Utils 类挂载凭据

public class AWSOauth 

    public static String getOAuthAWS(Context context, String fileName)  throws Exception

        String secret = context.getResources().getString(R.string.s3_secret);
        String access = context.getResources().getString(R.string.s3_access_key);
        String bucket = context.getResources().getString(R.string.s3_bucket);

        return gerateOAuthAWS(secret, access, bucket,fileName);
    

    private static String gerateOAuthAWS(String secretKey, String accessKey, String bucket, String imageName) throws Exception 

        String contentType = "image/jpeg";

        DateTimeFormatter fmt = DateTimeFormat.forPattern("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z").withLocale(Locale.US);
        String ZONE = "GMT";
        DateTime dt = new DateTime();
        DateTime dtLondon = dt.withZone(DateTimeZone.forID(ZONE)).plusHours(1);
        String formattedDate = dtLondon.toString(fmt);

        String resource = "/" + bucket + "/" + imageName;

        String stringToSign = "PUT" + "\n\n" + contentType + "\n" + formattedDate + "\n" + resource;

        Mac hmac = Mac.getInstance("HmacSHA1");
        hmac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA1"));

        String signature = ( Base64.encodeToString(hmac.doFinal(stringToSign.getBytes("UTF-8")), Base64.DEFAULT)).replaceAll("\n", "");

        String oauthAWS = "AWS " + accessKey + ":" + signature;

        return  oauthAWS;
    

最后提出请求的方法

 public void uploadImage(RequestBody body, String fileName)

        String bucket = getString(R.string.s3_bucket);

        Retrofit restAdapter = new Retrofit.Builder()
                .baseUrl("http://" + bucket + ".s3.amazonaws.com")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        AwsS3 service = restAdapter.create(AwsS3.class);

        DateTimeFormatter fmt = DateTimeFormat.forPattern("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z").withLocale(Locale.US);
        String ZONE = "GMT";
        DateTime dt = new DateTime();
        DateTime dtLondon = dt.withZone(DateTimeZone.forID(ZONE)).plusHours(1);
        String formattedDate = dtLondon.toString(fmt);

        try 

            String oauth = AWSOauth.getOAuthAWS(getApplicationContext(), fileName);

            Call<String> call = service.upload(fileName, body.contentLength(), "/**", bucket + ".s3.amazonaws.com", formattedDate,  body.contentType().toString(), oauth, body);
            call.enqueue(new Callback<String>() 
                @Override
                public void onResponse(Response<String> response) 
                    Log.d("tag", "response : " + response.body());
                

                @Override
                public void onFailure(Throwable t) 
                    Log.d("tag", "response : " + t.getMessage());
                
            );

         catch (Exception e) 
            e.printStackTrace();
        
    

感谢任何帮助,在此先感谢!

【问题讨论】:

没有抛出异常?请求的状态码是什么? 无异常,图片上传,响应200 OK。 你说图片上传成功,是不是也对比了md5sum?上传成功后,尝试比较你上传的对象的md5sum。这样你就可以确定对象根本没有改变。你也说when I try open the image but it is damaged..你想在桌面上查看它吗? 我也尝试打开浏览器和桌面! @A.Anderson 您是否有您尝试上传的图像/对象的样本(任何公共链接?)? 【参考方案1】:

我使用了 Retrofit 2 解决方案 我在界面中使用Body 而不是Part 作为您的RequestBody

@PUT("")
Call<String> nameAPI(@Url String url, @Body RequestBody body);

和java代码

// Prepare image file
File file = new File(pathImg);
RequestBody requestBody = RequestBody.create(MediaType.parse("image/jpeg"), file);

Call<String> call = SingletonApiServiceS3.getInstance().getService().nameAPI(
        path,
       requestBody
);
call.enqueue(new Callback<String>() 
    @Override
    public void onResponse(Call<String> call, final Response<String> response) 

        if (response.isSuccessful()) 
            // Your handling
         else 
            // Your handling
       
   

   @Override
   public void onFailure(Call<String> call, Throwable t) 
       Toast.makeText(getContext(), "onFailure : "+t.getMessage().toString(),Toast.LENGTH_SHORT).show();
   
);

【讨论】:

s3 PUT 调用需要这个,非常感谢 这有帮助,谢谢。我删除了@Multipart 并添加了 Body【参考方案2】:

我有同样的问题,当我使用 Fiddler 检查 HTTP 请求内容时,我发现改造 2.0.0 beta1 与 1.9.0 不同。

在我的问题中,HTTP 请求内容的不同会阻止服务器获取正确的数据。

为了制作相同的 HTTP 请求内容,我使用改造 2.0.0 deta1 执行后续步骤。


在改造服务中,为http请求添加form-data header;

@Headers("Content-Type: multipart/form-data;boundary=95416089-b2fd-4eab-9a14-166bb9c5788b")

int retrofit 2.0.0 deta1,使用@Multipart的头部会得到这样的数据:

内容类型:多部分/混合

因为默认值是混合的,并且没有边界标题。


不要使用@Multipart上传文件,只使用@BodyRequestBody

如果你使用@Multipart请求服务器,你必须通过param(file)

@Part(key),那么你会遇到一个新问题。可能是改造 2.0.0beta1 有一个 BUG ...,@Multipart 生成一个错误的 http 请求,用 1.9.0 编译。


调用方法时,需要将MultipartRequestBody传递给@Body RequestBody

使用MultipartBuilder创建MultipartRequestBody,当你新建MultipartBuilder时,调用这个consturt:

new MultipartBuilder("95416089-b2fd-4eab-9a14-166bb9c5788b")

参数是你设置的int @headers(boundary=)

builder.addFormDataPart(String name, String filename, RequestBody value)

此方法将帮助形成如下 int HTTP 请求内容的数据:

内容配置:表单数据;名称="imgFile"; 文件名="IMG_20150911_113029.jpg" 内容类型:image/jpg 内容长度:1179469

RequestBody 值是您在代码中生成的值。

我只是暂时解决了这个问题。

希望能帮到你!

【讨论】:

他们的方法似乎没问题,我发现 s3 AWS 需要在请求正文中发送内容,而不是作为多部分发送。 我对部件的 Content-Length 有疑问,Retrofit 为 RequestBody 和每个部件自动添加 Content-Length,服务器不允许这样做(我通过 Postman 测试了 API,它可以工作好吧),github.com/square/okhttp/issues/2604你有什么想法吗?还是我必须扔掉改造?【参考方案3】:
RequestBody avatarBody = RequestBody.create(MediaType.parse("image"),file);
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), avatarBody);


@Multipart
@POST(url)
Call<ResponseBody> uploadImageAmazon(
            @Part MultipartBody.Part filePart);

我有同样的经历,通过https://github.com/square/retrofit/issues/2424这个解决方案解决了

【讨论】:

【参考方案4】:

您正在发送多部分有效负载,但强制 Content-type 为 image/jpeg。您的 jpg 已损坏,因为 S3 可能将多部分标头保存到您的 jpg 文件中,因为您告诉它整个消息是 JPG。由于您实际上没有要发送的多个部分,因此您可以删除 Multipart 注释并使用 Body 而不是 Part 作为您的 RequestBody

public interface AwsS3 

    @PUT("/Key")
    Call<String> upload(@Path("Key") String Key,
                @Header("Content-Length") long length,
                @Header("Accept") String accept,
                @Header("Host") String host,
                @Header("Date") String date,
                @Header("Content-type") String contentType,
                @Header("Authorization") String authorization,
                @Body RequestBody body);

您还应该能够删除显式设置 Content-typeContent-length 标头。

【讨论】:

有道理,我以后试试这个方法! 所以我试过了,但效果不好。图片发送不正确!【参考方案5】:

我没有使用 Retrofit 2,只是 Retrofit 1,所以 YMMV,但我相信做你想做的事情的典型方法是在你尝试使用 RequestBody 的地方使用 TypedFile。

我猜 Retrofit 在内部使用 RequestBody。

您将创建 TypedFile,如下所示:

TypedFile typedFile = new TypedFile("multipart/form-data", new File("path/to/your/file"));

你的界面会是:

   @Multipart
    @PUT("/Key")
    Call<String> upload(@Path("Key") String Key,
                @Header("Content-Length") long length,
                @Header("Accept") String accept,
                @Header("Host") String host,
                @Header("Date") String date,
                @Header("Content-type") String contentType,
                @Header("Authorization") String authorization,
                @Part("Body") TypedFile body);

有一个很好的例子 https://futurestud.io/blog/retrofit-how-to-upload-files/

【讨论】:

感谢您的回答,我认为我的请求正文有些问题,因为请求执行正常,但我使用的是 Retrofit 2,TypedFile 已从 lib 中删除,我无法降级,我需要使用版本 2。 你是否考虑过放弃显式的 Content-Type 和 Content-Length 标头,让 Retrofit 从 RequestBody 中获取它们? 这不起作用 AWS api rest 要求发送此标头。【参考方案6】:

您可以使用改造 2 上传图像/文件

@Multipart
@POST("/api/attachment")
Call<JsonPrimitive> addAttachment(@Part MultipartBody.Part imageFile);

现在拨打电话:

 RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
    MultipartBody.Part imageFileBody = MultipartBody.Part.createFormData("file", file.getName(), requestBody);

注意:请确保您使用的是 retrofit2,因为由于某种原因我无法使用 retrofit1 库上传图片。

【讨论】:

以上是关于带有 Android Retrofit V2 库的 AWS S3 Rest API,上传的图像已损坏的主要内容,如果未能解决你的问题,请参考以下文章

带有 10 个标记的 Android 基本 API v2 MapActivity outOfMemory

Android Retrofit详解

Android Retrofit详解

如何在Android上通过retrofit2调用带有Cognito Credentials的API网关?

Android – 使用带有 Jetpack Compose 的 Retrofit 库进行 JSON 解析

Android – 使用带有 Jetpack Compose 的 Retrofit 库进行 JSON 解析