简体   繁体   English

从c#中的HTTPWebResponse.GetResponseStream()上传到S3

[英]Upload to S3 from HTTPWebResponse.GetResponseStream() in c#

I am trying to upload from an HTTP stream directly to S3, without storing in memory or as a file first. 我试图从HTTP流直接上传到S3,而不是先存储在内存中或作为文件。 I am already doing this with Rackspace Cloud Files as HTTP to HTTP, however the AWS authentication is beyond me so am trying to use the SDK. 我已经使用Rackspace Cloud Files作为HTTP到HTTP进行此操作,但AWS身份验证超出了我的范围,因此我尝试使用SDK。

The problem is the upload stream is failing with this exception: 问题是上传流失败并出现此异常:

"This stream does not support seek operations."

I've tried with PutObject and TransferUtility.Upload , both fail with the same thing. 我尝试过使用PutObjectTransferUtility.Upload ,两者都失败了。

Is there any way to stream into S3 as the stream comes in, rather than buffering the whole thing to a MemoryStream or FileStream ? 有什么方法可以在流进入时流入S3,而不是将整个事件缓冲到MemoryStreamFileStream

or is there any good examples of doing the authentication into S3 request using HTTPWebRequest, so I can duplicate what I do with Cloud Files? 或者是否有使用HTTPWebRequest对S3请求进行身份验证的好例子,所以我可以复制我对Cloud Files的处理方式?

Edit: or is there a helper function in the AWSSDK for generating the authorization header? 编辑: 或者 AWSSDK中是否有帮助函数用于生成授权标头?

CODE: 码:

This is the failing S3 part (both methods included for completeness): 这是失败的S3部分(两个方法都包括完整性):

string uri = RSConnection.StorageUrl + "/" + container + "/" + file.SelectSingleNode("name").InnerText;
var req = (HttpWebRequest)WebRequest.Create(uri);
req.Headers.Add("X-Auth-Token", RSConnection.AuthToken);
req.Method = "GET";

using (var resp = req.GetResponse() as HttpWebResponse)
{
    using (Stream stream = resp.GetResponseStream())
    {
        Amazon.S3.Transfer.TransferUtility trans = new Amazon.S3.Transfer.TransferUtility(S3Client);
        trans.Upload(stream, config.Element("root").Element("S3BackupBucket").Value, container + file.SelectSingleNode("name").InnerText);

        //Use EITHER the above OR the below

        PutObjectRequest putReq = new PutObjectRequest();
        putReq.WithBucketName(config.Element("root").Element("S3BackupBucket").Value);
        putReq.WithKey(container + file.SelectSingleNode("name").InnerText);
        putReq.WithInputStream(Amazon.S3.Util.AmazonS3Util.MakeStreamSeekable(stream));
        putReq.WithMetaData("content-length", file.SelectSingleNode("bytes").InnerText);

        using (S3Response putResp = S3Client.PutObject(putReq))
        {

        }
    }

}

And this is how I do it successfully from S3 to Cloud Files: 这就是我从S3到云文件成功的方法:

using (GetObjectResponse getResponse = S3Client.GetObject(new GetObjectRequest().WithBucketName(bucket.BucketName).WithKey(file.Key)))
{
    using (Stream s = getResponse.ResponseStream)
    {
        //We can stream right from s3 to CF, no need to store in memory or filesystem.                                            
        var req = (HttpWebRequest)WebRequest.Create(uri);
        req.Headers.Add("X-Auth-Token", RSConnection.AuthToken);
        req.Method = "PUT";

        req.AllowWriteStreamBuffering = false;
        if (req.ContentLength == -1L)
            req.SendChunked = true;


        using (Stream stream = req.GetRequestStream())
        {
            byte[] data = new byte[32768];
            int bytesRead = 0;
            while ((bytesRead = s.Read(data, 0, data.Length)) > 0)
            {
                stream.Write(data, 0, bytesRead);
            }
            stream.Flush();
            stream.Close();
        }
        req.GetResponse().Close();
    }
}   

As no-one answering seems to have done it, I spent the time working it out based on guidance from Steve's answer: 由于没有人回答似乎已经做过,我根据史蒂夫的回答指导我花时间解决问题:

In answer to this question "is there any good examples of doing the authentication into S3 request using HTTPWebRequest, so I can duplicate what I do with Cloud Files?", here is how to generate the auth header manually: 回答这个问题“有没有使用HTTPWebRequest对S3请求进行身份验证的好例子,所以我可以复制我对Cloud Files的处理方式?”,这里是如何手动生成auth头:

string today = String.Format("{0:ddd,' 'dd' 'MMM' 'yyyy' 'HH':'mm':'ss' 'zz00}", DateTime.Now);

string stringToSign = "PUT\n" +
    "\n" +
    file.SelectSingleNode("content_type").InnerText + "\n" +
    "\n" +
    "x-amz-date:" + today + "\n" +
    "/" + strBucketName + "/" + strKey;

Encoding ae = new UTF8Encoding();
HMACSHA1 signature = new HMACSHA1(ae.GetBytes(AWSSecret));
string encodedCanonical = Convert.ToBase64String(signature.ComputeHash(ae.GetBytes(stringToSign)));

string authHeader = "AWS " + AWSKey + ":" + encodedCanonical;

string uriS3 = "https://" + strBucketName + ".s3.amazonaws.com/" + strKey;
var reqS3 = (HttpWebRequest)WebRequest.Create(uriS3);
reqS3.Headers.Add("Authorization", authHeader);
reqS3.Headers.Add("x-amz-date", today);
reqS3.ContentType = file.SelectSingleNode("content_type").InnerText;
reqS3.ContentLength = Convert.ToInt32(file.SelectSingleNode("bytes").InnerText);
reqS3.Method = "PUT";

Note the added x-amz-date header as HTTPWebRequest sends the date in a different format to what AWS is expecting. 请注意添加的x-amz-date标头,因为HTTPWebRequest以不同的格式将日期发送到AWS所期望的格式。

From there it was just a case of repeating what I was already doing. 从那里开始,只是重复我已经在做的事情。

Take a look at Amazon S3 Authentication Tool for Curl . 看看用于Curl的Amazon S3身份验证工具 From that web page: 从该网页:

Curl is a popular command-line tool for interacting with HTTP services. Curl是一种流行的命令行工具,用于与HTTP服务进行交互。 This Perl script calculates the proper signature, then calls Curl with the appropriate arguments. 此Perl脚本计算正确的签名,然后使用适当的参数调用Curl。

You could probably adapt it or its output for your use. 你可能会调整它或它的输出供你使用。

I think the problem is that according to the AWS Documentation Content-Length is required and you don't know what the length is until the stream has finished. 我认为问题在于,根据AWS文档,内容长度是必需的,并且在流完成之前您不知道长度是多少。

(I would guess the Amazon.S3.Util.AmazonS3Util.MakeStreamSeekable routine is reading the whole stream into memory to get around this problem which makes it unsuitable for your scenario.) (我猜想Amazon.S3.Util.AmazonS3Util.MakeStreamSeekable例程正在将整个流读入内存以解决这个问题,这使得它不适合您的场景。)

What you can do is read the file in chunks and upload them using MultiPart upload . 您可以做的是以块的形式读取文件并使用MultiPart上传来上传它们。

PS, I assume you know the C# source for the AWSSDK for dotnet is on Github . PS,我假设你知道dotnet的AWSSDK的C#源是在Github上

This is a true hack (which would probably break with a new implementation of the AWSSDK), and it requires knowledge of the length of the file being requested, but if you wrap the response stream as shown with this class (a gist) as shown below: 这是一个真正的黑客攻击(它可能会破坏AWSSDK的新实现),并且它需要知道所请求文件的长度,但是如果你用这个类(一个要点)所示包装响应流,如图所示下面:

long length = fileLength;  

you can get file length in several ways. 你可以用几种方式获得文件长度。 I am uploading from a dropbox link, so they give me the length along with the url. 我从Dropbox链接上传,所以他们给我长度和网址。 Alternatively, you can perform a HEAD request and get the Content-Length. 或者,您可以执行HEAD请求并获取Content-Length。

string uri = RSConnection.StorageUrl + "/" + container + "/" + file.SelectSingleNode("name").InnerText;
var req = (HttpWebRequest)WebRequest.Create(uri);
req.Headers.Add("X-Auth-Token", RSConnection.AuthToken);
req.Method = "GET";

using (var resp = req.GetResponse() as HttpWebResponse)
{
    using (Stream stream = resp.GetResponseStream())
    {
        //I haven't tested this path
        Amazon.S3.Transfer.TransferUtility trans = new Amazon.S3.Transfer.TransferUtility(S3Client);
        trans.Upload(new HttpResponseStream(stream, length), config.Element("root").Element("S3BackupBucket").Value, container + file.SelectSingleNode("name").InnerText);

        //Use EITHER the above OR the below
        //I have tested this with dropbox data
        PutObjectRequest putReq = new PutObjectRequest();
        putReq.WithBucketName(config.Element("root").Element("S3BackupBucket").Value);
        putReq.WithKey(container + file.SelectSingleNode("name").InnerText);
        putReq.WithInputStream(new HttpResponseStream(stream, length)));
        //These are necessary for really large files to work
        putReq.WithTimeout(System.Threading.Timeout.Infinite);
        putReq.WithReadWriteTimeout(System.Thread.Timeout.Infinite);


        using (S3Response putResp = S3Client.PutObject(putReq))
        {

        }
    }

}

The hack is overriding the Position and Length properties, and returning 0 for Position{get}, noop'ing Position{set}, and returning the known length for Length. 该hack覆盖了Position和Length属性,并为Position {get}返回0,noop'ing Position {set},并返回Length的已知长度。

I recognize that this might not work if you don't have the length or if the server providing the source does not support HEAD requests and Content-Length headers. 我认识到如果您没有长度或者提供源的服务器不支持HEAD请求和Content-Length标头,这可能不起作用。 I also realize it might not work if the reported Content-Length or the supplied length doesn't match the actual length of the file. 我也意识到如果报告的Content-Length或提供的长度与文件的实际长度不匹配,它可能不起作用。

In my test, I also supply the Content-Type to the PutObjectRequest, but I don't that that is necessary. 在我的测试中,我还向PutObjectRequest提供了Content-Type,但我不认为这是必要的。

As sgmoore said, the problem is that your content length is not seekable from the HTTP response. 正如sgmoore所说,问题是你的内容长度不能从HTTP响应中找到。 However HttpWebResponse does have a content length property available. 但是,HttpWebResponse确实具有可用的内容长度属性。 So you can actually form your Http post request to S3 yourself instead of using the Amazon library. 因此,您实际上可以自己形成您的Http post请求,而不是使用Amazon库。

Here's another Stackoverflow question that managed to do that with what looks like full code to me. 这是另一个Stackoverflow问题 ,设法用看起来像我的完整代码。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM