diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..2869bbf2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +## CHANGE LOG + +### v2.4.0 + +2013-02-19 + +Issue [#10](https://github.com/qiniu/csharp-sdk/pull/10): + +- QBox.Auth.AuthPolicy 增加成员:CallbackBodyType, Escape, AsyncOps, ReturnBody +- DownloadToken支持:增加 QBox.Auth.DownloadPolicy 类 +- 增加 PutAuth 支持:增加 QBox.Auth.PutAuthClient 类 +- 非兼容调整:QBox.RS.Client 改名为 QBox.RPC.Client +- 简易断点续上传支持: 增加 QBox.RS.ResumablePut 类 +- hotfix: 修复了 Base64 编码不支持中文的情况(QBox/Util) + diff --git a/Demo/Demo.cs b/Demo/Demo.cs index ca1adce3..7c9d5703 100644 --- a/Demo/Demo.cs +++ b/Demo/Demo.cs @@ -2,6 +2,7 @@ using QBox.Auth; using QBox.RS; using QBox.FileOp; +using QBox.RPC; namespace QBox.Demo { @@ -10,39 +11,41 @@ public class Demo public static string bucketName; public static string key; public static string localFile; + public static string bigkey; + public static string bigFile; public static string DEMO_DOMAIN; public static Client conn; public static RSService rs; - public static ImageOp imageOp; public static void Main() { Config.ACCESS_KEY = ""; Config.SECRET_KEY = ""; - bucketName = "csharpbucket"; - DEMO_DOMAIN = "csharpbucket.dn.qbox.me"; - localFile = "Resource/gogopher.jpg"; + bucketName = "yourbucket"; + DEMO_DOMAIN = bucketName + ".qiniudn.com"; key = "gogopher.jpg"; + localFile = "Resource/gogopher.jpg"; + bigkey = key; + bigFile = localFile; conn = new DigestAuthClient(); rs = new RSService(conn, bucketName); - imageOp = new ImageOp(conn); MkBucket(); RSClientPutFile(); - Get(); - Stat(); - Publish(); - UnPublish(); - Delete(); + Get(key); + ResumablePutFile(); + Stat(bigkey); + Delete(key); Drop(); MkBucket(); RSPutFile(); - Publish(); ImageOps(); + MakeDownloadToken(); + Console.ReadLine(); } @@ -70,21 +73,13 @@ public static void RSPutFile() public static void RSClientPutFile() { - Console.WriteLine("\n==> PutAuth"); - PutAuthRet putAuthRet = rs.PutAuth(); - PrintRet(putAuthRet); - if (putAuthRet.OK) - { - Console.WriteLine("Expires: " + putAuthRet.Expires.ToString()); - Console.WriteLine("Url: " + putAuthRet.Url); - } - else - { - Console.WriteLine("Failed to PutAuth"); - } + Console.WriteLine("\n===> RSClient Generate UpToken"); + var authPolicy = new AuthPolicy(bucketName, 3600); + string upToken = authPolicy.MakeAuthTokenString(); + Console.WriteLine("upToken: " + upToken); - Console.WriteLine("\n===> RSClient.PutFile"); - PutFileRet putFileRet = RSClient.PutFile(putAuthRet.Url, bucketName, key, null, localFile, null, "key="); + Console.WriteLine("\n===> RSClient.PutFileWithUpToken"); + PutFileRet putFileRet = RSClient.PutFileWithUpToken(upToken, bucketName, key, null, localFile, null, "key="); PrintRet(putFileRet); if (putFileRet.OK) { @@ -92,16 +87,17 @@ public static void RSClientPutFile() } else { - Console.WriteLine("Failed to RSClient.PutFile"); + Console.WriteLine("Failed to RSClient.PutFileWithUpToken"); } + } - Console.WriteLine("\n===> Generate UpToken"); + public static void ResumablePutFile() + { + Console.WriteLine("\n===> ResumablePut.PutFile"); var authPolicy = new AuthPolicy(bucketName, 3600); string upToken = authPolicy.MakeAuthTokenString(); - Console.WriteLine("upToken: " + upToken); - - Console.WriteLine("\n===> RSClient.PutFileWithUpToken"); - putFileRet = RSClient.PutFileWithUpToken(upToken, bucketName, key, null, localFile, null, "key="); + PutAuthClient client = new PutAuthClient(upToken); + PutFileRet putFileRet = ResumablePut.PutFile(client, bucketName, bigkey, null, bigFile, null, "key="); PrintRet(putFileRet); if (putFileRet.OK) { @@ -109,13 +105,13 @@ public static void RSClientPutFile() } else { - Console.WriteLine("Failed to RSClient.PutFileWithUpToken"); + Console.WriteLine("Failed to ResumablePut.PutFile"); } } - public static void Get() + public static void Get(string key) { - Console.WriteLine("\n===> Get"); + Console.WriteLine("\n===> RSService.Get"); GetRet getRet = rs.Get(key, "attName"); PrintRet(getRet); if (getRet.OK) @@ -130,7 +126,7 @@ public static void Get() Console.WriteLine("Failed to Get"); } - Console.WriteLine("\n===> GetIfNotModified"); + Console.WriteLine("\n===> RSService.GetIfNotModified"); getRet = rs.GetIfNotModified(key, "attName", getRet.Hash); PrintRet(getRet); if (getRet.OK) @@ -146,9 +142,9 @@ public static void Get() } } - public static void Stat() + public static void Stat(string key) { - Console.WriteLine("\n===> Stat"); + Console.WriteLine("\n===> RSService.Stat"); StatRet statRet = rs.Stat(key); PrintRet(statRet); if (statRet.OK) @@ -164,9 +160,9 @@ public static void Stat() } } - public static void Delete() + public static void Delete(string key) { - Console.WriteLine("\n===> Delete"); + Console.WriteLine("\n===> RSService.Delete"); CallRet deleteRet = rs.Delete(key); PrintRet(deleteRet); if (!deleteRet.OK) @@ -177,7 +173,7 @@ public static void Delete() public static void Drop() { - Console.WriteLine("\n===> Drop"); + Console.WriteLine("\n===> RSService.Drop"); CallRet dropRet = rs.Drop(); PrintRet(dropRet); if (!dropRet.OK) @@ -186,32 +182,19 @@ public static void Drop() } } - public static void Publish() + public static void MakeDownloadToken() { - Console.WriteLine("\n===> Publish"); - CallRet publishRet = rs.Publish(DEMO_DOMAIN); - PrintRet(publishRet); - if (!publishRet.OK) - { - Console.WriteLine("Failed to Publish"); - } - } - - public static void UnPublish() - { - Console.WriteLine("\n===> UnPublish"); - CallRet publishRet = rs.Unpublish(DEMO_DOMAIN); - PrintRet(publishRet); - if (!publishRet.OK) - { - Console.WriteLine("Failed to UnPublish"); - } + Console.WriteLine("\n===> Auth.MakeDownloadToken"); + string pattern = "*/*"; + var downloadPolicy = new DownloadPolicy(pattern, 3600); + string dnToken = downloadPolicy.MakeAuthTokenString(); + Console.WriteLine("dnToken: " + dnToken); } public static void ImageOps() { - Console.WriteLine("\n===> ImageInfo"); - ImageInfoRet infoRet = imageOp.ImageInfo("http://" + DEMO_DOMAIN + "/" + key); + Console.WriteLine("\n===> FileOp.ImageInfo"); + ImageInfoRet infoRet = ImageOp.ImageInfo("http://" + DEMO_DOMAIN + "/" + key); PrintRet(infoRet); if (infoRet.OK) { @@ -225,34 +208,34 @@ public static void ImageOps() Console.WriteLine("Failed to ImageInfo"); } - Console.WriteLine("\n===> ImageExif"); - CallRet exifRet = imageOp.ImageExif("http://" + DEMO_DOMAIN + "/" + key); + Console.WriteLine("\n===> FileOp.ImageExif"); + CallRet exifRet = ImageOp.ImageExif("http://" + DEMO_DOMAIN + "/" + key); PrintRet(exifRet); if (!exifRet.OK) { Console.WriteLine("Failed to ImageExif"); } - Console.WriteLine("\n===> ImageViewUrl"); + Console.WriteLine("\n===> FileOp.ImageViewUrl"); ImageViewSpec viewSpec = new ImageViewSpec{Mode = 0, Width = 200, Height= 200}; - string viewUrl = imageOp.ImageViewUrl("http://" + DEMO_DOMAIN + "/" + key, viewSpec); + string viewUrl = ImageOp.ImageViewUrl("http://" + DEMO_DOMAIN + "/" + key, viewSpec); Console.WriteLine("ImageViewUrl 1:" + viewUrl); viewSpec.Quality = 1; viewSpec.Format = "gif"; - viewUrl = imageOp.ImageViewUrl("http://" + DEMO_DOMAIN + "/" + key, viewSpec); + viewUrl = ImageOp.ImageViewUrl("http://" + DEMO_DOMAIN + "/" + key, viewSpec); Console.WriteLine("ImageViewUrl 2:" + viewUrl); viewSpec.Quality = 90; viewSpec.Sharpen = 10; viewSpec.Format = "png"; - viewUrl = imageOp.ImageViewUrl("http://" + DEMO_DOMAIN + "/" + key, viewSpec); + viewUrl = ImageOp.ImageViewUrl("http://" + DEMO_DOMAIN + "/" + key, viewSpec); Console.WriteLine("ImageViewUrl 3:" + viewUrl); - Console.WriteLine("\n===> ImageMogrifyUrl"); + Console.WriteLine("\n===> FileOp.ImageMogrifyUrl"); ImageMogrifySpec mogrSpec = new ImageMogrifySpec { Thumbnail = "!50x50r", Gravity = "center", Rotate = 90, Crop = "!50x50", Quality = 80, AutoOrient = true }; - string mogrUrl = imageOp.ImageMogrifyUrl("http://" + DEMO_DOMAIN + "/" + key, mogrSpec); + string mogrUrl = ImageOp.ImageMogrifyUrl("http://" + DEMO_DOMAIN + "/" + key, mogrSpec); Console.WriteLine("ImageMogrifyUrl:" + mogrUrl); Console.WriteLine("\n===> Get"); @@ -269,7 +252,7 @@ public static void ImageOps() { Console.WriteLine("Failed to Get"); } - Console.WriteLine("\n===> ImageMogrifySaveAs"); + Console.WriteLine("\n===> FileOp.ImageMogrifySaveAs"); PutFileRet saveAsRet = rs.ImageMogrifySaveAs(getRet.Url, mogrSpec, key + ".mogr-save-as"); PrintRet(saveAsRet); if (saveAsRet.OK) diff --git a/Docs/README.md b/Docs/README.md new file mode 100644 index 00000000..d6b7b4b2 --- /dev/null +++ b/Docs/README.md @@ -0,0 +1,358 @@ +title: C# SDK | 七牛云存储 +--- + +# C# SDK 使用指南 + + +此 SDK 适用于 .NET4 及以上版本。 + +SDK 在这里:[https://github.com/qiniu/csharp-sdk/tags](https://github.com/qiniu/csharp-sdk/tags) + +**目录** + +- [1 注册账号](#register) +- [2 授权机制](#auth) + - [2.1 上传文件授权](#auth-up) + - [2.2 下载文件授权](#auth-dn) + - [2.3 文件管理授权](#auth-mgr) +- [3 存储接口](#store) + - [3.1 上传文件](#up) + - [3.2 下载文件](#dn) + - [3.3 删除文件](#del) + - [3.4 获取文件信息](#stat) +- [4 文件处理接口](#fop) + - [4.1 图片处理](#imgfop) + - [4.1.1 获取图片基础信息](#imageinfo) + - [4.1.2 获取图片EXIF信息](#imageexif) + - [4.1.3 图片缩略图](#imageview) + - [4.1.4 高级图片处理](#imagemogrify) + + + +## 1 注册账号 + +登陆[开发者网站](https://dev.qiniutek.com/signup)注册七牛云存储账号,注册成功后,你会获得一对 AccessKey 和 SecretKey。 + + + +## 2 授权机制 + + + +### 2.1 上传文件授权 + +上传文件需要 UpToken 来取得服务端授权。 +UpToken 是由 AuthPolicy 以及 AccessKey 和 SecretKey 生成的。 + + public class AuthPolicy + { + public string Scope { get; set; } + public long Deadline { get; set; } + public string CallbackUrl { get; set; } + public string CallbackBodyType { get; set; } + public bool Escape { get; set; } + public string AsyncOps { get; set; } + public string ReturnBody { get; set; } + } + +各字段的含义见[这里](http://docs.qiniutek.com/v3/api/io/#upload-token-algorithm)。 + +生成 UpToken 例子: + + using QBox.Auth; + + var authPolicy = new AuthPolicy(bucketName, 3600); + authPolicy.CallbackUrl = "www.example.com/qiniu/callback"; + authPolicy.CallbackBodyType = "application/json" + string upToken = authPolicy.MakeAuthTokenString(); + + + +### 2.2 下载私有文件授权 + +下载私有文件需要 DownloadToken 来取得服务端授权。 +DownloadToken 是由 DownloadPolicy 以及 AccessKey 和 SecretKey 生成的。 + + public class DownloadPolicy + { + public string Pattern { get; set; } + public long Deadline { get; set; } + } + +各参数的含义见[这里](http://docs.qiniutek.com/v3/api/io/#private-download)。 + +生成 DownloadToken 例子: + + using QBox.Auth; + + string pattern = "*/*"; + var downloadPolicy = new DownloadPolicy(pattern, 3600); + string downloadToken = downloadPolicy.MakeAuthTokenString(); + + + +### 2.3 文件管理授权 + +文件管理,比如删除文件,获取文件元信息等操作需要提供 AccessToken(放在 HTTP Header 里面) 来取得服务端授权。 +AccessToken 是由 HTTP 请求的 URL,BodyType,Body 以及 AccessKey 和 SecretKey 生成的。 + +获取自动为请求添加 AccessToken 的客户端: + + using QBox.Auth; + using QBox.RS; + + conn = new DigestAuthClient(); + rs = new RSService(conn, bucketName); + +然后就可以用 rs 来进行文件管理操作。 + + + +## 3 存储接口 + + + +### 3.1 上传文件 + +上传文件需要 upToken,上传 API 为: + + public static PutFileRet PutFileWithUpToken( + string upToken, string tableName, string key, string mimeType, + string localFile, string customMeta, string callbackParam) + +例子: + + using QBox.Auth; + using QBox.RS; + + var authPolicy = new AuthPolicy(bucketName, 3600); + authPolicy.CallbackUrl = "www.example.com/qiniu/callback"; + authPolicy.CallbackBodyType = "application/json" + string upToken = authPolicy.MakeAuthTokenString(); + + string callbackParam = "bucket=&key=" + PutFileRet ret = RSClient.PutFileWithUpToken(upToken, tableName, key, null, localFile, null, callbackParam); + if (ret.OK) Console.Writeline("upload and callback ok"); + +此例子是上传一个文件然后将上传的 bucket 和 key 信息回调给 www.example.com/qiniu/callback。 + +如果上传的文件比较大(大于4M),可以使用断点续传,其将文件在内部切割成多个 4M 的块, +一块块上传,以免直接上传出现超时或用户体验差的问题,断点续传 API 为: + + public static PutFileRet PutFile( + Client client, string tableName, string key, string mimeType, + string localFile, string customMeta, string callbackParam) + +client 参数是能自动为请求在 HTTP Header 中添加 UpToken 的 Client。 + +例子: + + using QBox.Auth; + using QBox.RS; + + var authPolicy = new AuthPolicy(bucketName, 3600); + string upToken = authPolicy.MakeAuthTokenString(); + PutAuthClient client = new PutAuthClient(upToken); + + PutFileRet ret = ResumablePut.PutFile(client, tableName, key, null, bigFile, null, null); + if (ret.OK) Console.Writeline("resumable put ok"); + + + +### 3.2 下载文件 + +对于公有资源,访问方式为: + + http://<绑定域名>/key + +对于[私有资源](http://docs.qiniutek.com/v3/api/io/#private-download),需要 downloadToken,访问方式为: + + http://<绑定域名>/key?token= + + + +### 3.3 删除文件 + +需要 AccessToken 授权,删除 API 为: + + public CallRet Delete(string key) + +例子: + + using QBox.Auth; + using QBox.RS; + + conn = new DigestAuthClient(); + rs = new RSService(conn, bucketName); + CallRet ret = rs.Delete(key); + if (ret.OK) Console.Write("delete ok"); + + + +### 3.4 获取文件元信息 + +需要 AccessToken 授权,获取元信息 API 为: + + public class StatRet : CallRet + { + public string Hash { get; private set; } + public long FileSize { get; private set; } + public long PutTime { get; private set; } + public string MimeType { get; private set; } + } + public StatRet Stat(string key); + +例子: + + using QBox.Auth; + using QBox.RS; + + conn = new DigestAuthClient(); + rs = new RSService(conn, bucketName); + StatRet ret = rs.Stat(key); + if (ret.OK) + { + Console.WriteLine("Hash: " + ret.Hash); + Console.WriteLine("FileSize: " + ret.FileSize); + Console.WriteLine("PutTime: " + ret.PutTime); + Console.WriteLine("MimeType: " + ret.MimeType); + } + + + +## 4 文件处理接口 + + + +### 4.1 图像处理 + + + +#### 4.1.1 获取图片信息 + +获取图片基本信息,API 为: + + public class ImageInfoRet : CallRet + { + public string Format { get; private set; } + public int Width { get; private set; } + public int Height { get; private set; } + public string ColorModel { get; private set; } + } + public static ImageInfoRet ImageInfo(string url); + +例子: + + using QBox.FileOp; + + ImageInfoRet ret = ImageOp.ImageInfo("http://yourbucket.qiniudn.com/" + key); + if (ret.OK) + { + Console.WriteLine("Format: " + ret.Format); + Console.WriteLine("Width: " + ret.Width); + Console.WriteLine("Heigth: " + ret.Height); + Console.WriteLine("ColorModel: " + ret.ColorModel); + } + + + +#### 4.1.2 获取图片EXIF信息 + +获取图片 EXIF 信息,API 为: + + public static CallRet ImageExif(string url); + +例子: + + using QBox.FileOp; + using QBox.RPC; + + CallRet ret = ImageOp.ImageExif("http://yourbucket.qiniudn.com/" + key); + if (ret.OK) Console.Writeline("Exif:\n" + ret.Response); + + + +#### 4.1.3 图片缩略图 + +获取缩略图URL,API 为: + + public class ImageViewSpec + { + public int Mode { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public int Quality { get; set; } + public string Format { get; set; } + public int Sharpen { get; set; } + + public string MakeSpecString() + } + +具体字段含义见[这里](http://docs.qiniutek.com/v3/api/foimg/#imageView) + +例子: + + using QBox.FileOp; + + ImageViewSpec viewSpec = new ImageViewSpec{Mode = 1, Width = 200, Height= 200}; + string viewUrl = ImageOp.ImageViewUrl("http://yourbucket.qiniudn.com/" + key, viewSpec); + Console.WriteLine("ImageViewUrl:" + viewUrl); + + + +#### 4.1.4 高级图片处理 + +可以对存储中的图片做缩略、裁剪、旋转和格式转化处理,API 为: + + public class ImageMogrifySpec + { + public string Thumbnail { get; set; } + public string Gravity { get; set; } + public string Crop { get; set; } + public int Quality { get; set; } + public int Rotate { get; set; } + public string Format { get; set; } + public bool AutoOrient { get; set; } + + public string MakeSpecString() + } + +具体字段含义见[这里](http://docs.qiniutek.com/v3/api/foimg/#imageMogr)。 + +例子: + + using QBox.FileOp; + + ImageMogrifySpec mogrSpec = new ImageMogrifySpec { + Thumbnail = "!50x50r", Gravity = "center", Rotate = 90, + Crop = "!50x50", Quality = 80, AutoOrient = true}; + string mogrUrl = ImageOp.ImageMogrifyUrl("http://yourbucket.qiniudn.com/" + key, mogrSpec); + Console.WriteLine("ImageMogrifyUrl:" + mogrUrl); + +可以将处理后的图片持久化到云存储,这里需要一个结过授权的图片 URL, 可以用 Get 接口获取的,所需 API 为: + + public class GetRet : CallRet + { + public string Hash { get; private set; } + public long FileSize { get; private set; } + public string MimeType { get; private set; } + public string Url { get; private set; } + } + public GetRet Get(string key, string attName); + public PutFileRet ImageMogrifySaveAs(string url, ImageMogrifySpec spec, string key) + +例子: + + using QBox.Auth; + using QBox.RS; + using QBox.FileOp; + + conn = new DigestAuthClient(); + rs = new RSService(conn, bucketName); + GetRet getRet = rs.Get(key, "save-as"); + + if (getRet.OK) + { + PutFileRet saveAsRet = rs.ImageMogrifySaveAs(getRet.Url, mogrSpec, key + ".save-as.jpg"); + if (saveAsRet.OK) Console.Writeline("mogrify ok and save to :.save-as.jpg"); + } diff --git a/QBox/Auth/AuthPolicy.cs b/QBox/Auth/AuthPolicy.cs index be68d919..1bd4a9c6 100644 --- a/QBox/Auth/AuthPolicy.cs +++ b/QBox/Auth/AuthPolicy.cs @@ -4,6 +4,7 @@ using LitJson; using System.Security.Cryptography; using QBox.RS; +using QBox.Util; namespace QBox.Auth { @@ -12,7 +13,10 @@ public class AuthPolicy public string Scope { get; set; } public long Deadline { get; set; } public string CallbackUrl { get; set; } - public string ReturnUrl { get; set; } + public string CallbackBodyType { get; set; } + public bool Escape { get; set; } + public string AsyncOps { get; set; } + public string ReturnBody { get; set; } public AuthPolicy(string scope, long expires) { @@ -28,43 +32,22 @@ public string Marshal() JsonData data = new JsonData(); data["scope"] = Scope; data["deadline"] = Deadline; - if (CallbackUrl != null) + if (!String.IsNullOrEmpty(CallbackUrl)) data["callbackUrl"] = CallbackUrl; - if (ReturnUrl != null) - data["returnUrl"] = ReturnUrl; + if (!String.IsNullOrEmpty(CallbackBodyType)) + data["callbackBodyType"] = CallbackBodyType; + if (Escape) + data["escape"] = 1; + if (!String.IsNullOrEmpty(AsyncOps)) + data["asyncOps"] = AsyncOps; + if (!String.IsNullOrEmpty(ReturnBody)) + data["returnBody"] = ReturnBody; return data.ToJson(); } public byte[] MakeAuthToken() { - Encoding encoding = Encoding.ASCII; - byte[] accessKey = encoding.GetBytes(Config.ACCESS_KEY); - byte[] secretKey = encoding.GetBytes(Config.SECRET_KEY); - byte[] upToken = null; - try - { - byte[] policyBase64 = encoding.GetBytes(Base64UrlSafe.Encode(Marshal())); - byte[] digestBase64 = null; - using (HMACSHA1 hmac = new HMACSHA1(secretKey)) - { - byte[] digest = hmac.ComputeHash(policyBase64); - digestBase64 = encoding.GetBytes(Base64UrlSafe.Encode(digest)); - } - using (MemoryStream buffer = new MemoryStream()) - { - buffer.Write(accessKey, 0, accessKey.Length); - buffer.WriteByte((byte)':'); - buffer.Write(digestBase64, 0, digestBase64.Length); - buffer.WriteByte((byte)':'); - buffer.Write(policyBase64, 0, policyBase64.Length); - upToken = buffer.ToArray(); - } - } - catch (Exception e) - { - Console.WriteLine(e.ToString()); - } - return upToken; + return AuthToken.Make(Marshal()); } public string MakeAuthTokenString() diff --git a/QBox/Auth/AuthToken.cs b/QBox/Auth/AuthToken.cs new file mode 100644 index 00000000..ecc02b75 --- /dev/null +++ b/QBox/Auth/AuthToken.cs @@ -0,0 +1,45 @@ +using System; +using System.Text; +using System.IO; +using System.Security.Cryptography; +using QBox.RS; +using QBox.Util; + +namespace QBox.Auth +{ + public static class AuthToken + { + public static byte[] Make(string scope) + { + Encoding encoding = Encoding.ASCII; + byte[] accessKey = encoding.GetBytes(Config.ACCESS_KEY); + byte[] secretKey = encoding.GetBytes(Config.SECRET_KEY); + byte[] upToken = null; + try + { + byte[] policyBase64 = encoding.GetBytes(Base64UrlSafe.Encode(scope)); + byte[] digestBase64 = null; + using (HMACSHA1 hmac = new HMACSHA1(secretKey)) + { + byte[] digest = hmac.ComputeHash(policyBase64); + digestBase64 = encoding.GetBytes(Base64UrlSafe.Encode(digest)); + } + using (MemoryStream buffer = new MemoryStream()) + { + buffer.Write(accessKey, 0, accessKey.Length); + buffer.WriteByte((byte)':'); + buffer.Write(digestBase64, 0, digestBase64.Length); + buffer.WriteByte((byte)':'); + buffer.Write(policyBase64, 0, policyBase64.Length); + upToken = buffer.ToArray(); + } + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + return upToken; + } + + } +} diff --git a/QBox/Auth/DigestAuthClient.cs b/QBox/Auth/DigestAuthClient.cs index 1aca0e95..e156c270 100644 --- a/QBox/Auth/DigestAuthClient.cs +++ b/QBox/Auth/DigestAuthClient.cs @@ -3,6 +3,8 @@ using System.Net; using System.IO; using System.Security.Cryptography; +using QBox.Util; +using QBox.RPC; using QBox.RS; namespace QBox.Auth @@ -22,7 +24,12 @@ public override void SetAuth(HttpWebRequest request, Stream body) buffer.WriteByte((byte)'\n'); if (request.ContentType == "application/x-www-form-urlencoded" && body != null) { - body.CopyTo(buffer); + if (!body.CanSeek) + { + throw new Exception("stream can not seek"); + } + StreamUtil.Copy(body, buffer); + body.Seek(0, SeekOrigin.Begin); } byte[] digest = hmac.ComputeHash(buffer.ToArray()); string digestBase64 = Base64UrlSafe.Encode(digest); diff --git a/QBox/Auth/DownloadPolicy.cs b/QBox/Auth/DownloadPolicy.cs new file mode 100644 index 00000000..219a152a --- /dev/null +++ b/QBox/Auth/DownloadPolicy.cs @@ -0,0 +1,43 @@ +using System; +using System.Text; +using System.IO; +using System.Security.Cryptography; +using LitJson; +using QBox.RS; +using QBox.Util; + +namespace QBox.Auth +{ + public class DownloadPolicy + { + public string Pattern { get; set; } + public long Deadline { get; set; } + + public DownloadPolicy(string pattern, long expires) + { + Pattern = pattern; + DateTime begin = new DateTime(1970, 1, 1); + DateTime now = DateTime.Now; + TimeSpan interval = new TimeSpan(now.Ticks - begin.Ticks); + Deadline = (long)interval.TotalSeconds + expires; + } + + public string Marshal() + { + JsonData data = new JsonData(); + data["S"] = Pattern; + data["E"] = Deadline; + return data.ToJson(); + } + + public byte[] MakeAuthToken() + { + return AuthToken.Make(Marshal()); + } + + public string MakeAuthTokenString() + { + return Encoding.ASCII.GetString(MakeAuthToken()); + } + } +} diff --git a/QBox/Auth/PutAuthClient.cs b/QBox/Auth/PutAuthClient.cs new file mode 100644 index 00000000..5e489f21 --- /dev/null +++ b/QBox/Auth/PutAuthClient.cs @@ -0,0 +1,23 @@ +using System; +using System.Net; +using System.IO; +using QBox.RPC; + +namespace QBox.Auth +{ + public class PutAuthClient : Client + { + public string UpToken { get; set; } + + public PutAuthClient(string upToken) + { + UpToken = upToken; + } + + public override void SetAuth(HttpWebRequest request, Stream body) + { + string authHead = "UpToken " + UpToken; + request.Headers.Add("Authorization", authHead); + } + } +} diff --git a/QBox/Docs/README.md b/QBox/Docs/README.md deleted file mode 100644 index 58c45b18..00000000 --- a/QBox/Docs/README.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -title: C# SDK | 七牛云存储 ---- - -# C# SDK 使用指南 - - -此 SDK 适用于 .NET4 及以上版本。 - -SDK下载地址:[https://github.com/qiniu/csharp-sdk/tags](https://github.com/qiniu/csharp-sdk/tags) - - -**应用接入** - -- [获取 Access Key 和 Secret Key](#acc-appkey) -- [签名认证](#acc-auth) - -**云存储接口** - -- [新建资源表](#rs-NewService) -- [上传文件](#rs-PutFile) - - [服务器端上传](#server-PutFile) - - [客户端上传](#client-PutFileWithUpToke) -- [获取已上传文件信息](#rs-Stat) -- [下载文件](#rs-Get) -- [发布公开资源](#rs-Publish) -- [取消资源发布](#rs-Unpublish) -- [删除已上传的文件](#rs-Delete) -- [删除整张资源表](#rs-Drop) - -## 应用接入 - - - -### 1. 获取Access Key 和 Secret Key - -要接入七牛云存储,您需要拥有一对有效的 Access Key 和 Secret Key 用来进行签名认证。可以通过如下步骤获得: - -1. [开通七牛开发者帐号](https://dev.qiniutek.com/signup) -2. [登录七牛开发者自助平台,查看 Access Key 和 Secret Key](https://dev.qiniutek.com/account/keys) 。 - - - -### 2. 签名认证 - -首先,到 [https://github.com/qiniu/csharp-sdk](https://github.com/qiniu/csharp-sdk) 下载SDK源码。 - -然后,将 SDK 导入到您的 Visual C# 项目中,并编辑 QBox 目录下的 Config.cs 文件,确保其包含您从七牛开发者平台所获取的 [Access Key 和 Secret Key](#acc-appkey): - - public static string ACCESS_KEY = ""; - public static string SECRET_KEY = ""; - -在完成 Access Key 和 Secret Key 配置后,为了正常使用该 SDK 提供的功能,您还需要使用你获得的 Access Key 和 Secret Key 向七牛云存储服务器发出认证请求: - - DigestAuthClient conn = new DigestAuthClient(); - -请求成功后得到的 conn 即可用于您正常使用七牛云存储的一系列功能,接下来将一一介绍。 - -## 云存储接口 - - - -### 1. 新建资源表 - - // 首先定义资源表名 - string tableName = "tableName"; - - // 然后获得签名认证 - DigestAuthClient conn = new DigestAuthClient(); - - // 签名认证完成后,即可使用该认证来新建资源表 - RSService rs = new RSService(conn, tableName); - CallRet callRet = rs.MkBucket(); - - - -### 2. 上传文件 - -七牛云存储上传文件的方式分为服务器端上传和客户端上传两种。 - - - -#### 2.1 服务器端上传 - - // 调用资源表对象的 PutFile() 方法进行文件上传 - PutFileRet putFileRet = rs.PutFile(key, mimeType, filePath, customMeta); - - - -#### 2.2 客户端上传 - -##### 2.2.1 生成用于上传文件的临时凭证UpToken - -客户端上传文件之前需要取得上传授权 UpToken,UpToken 是服务器端颁发给客户端的上传凭证,参数 expires 对应的值则是该 UpToken 的有效期。 - - // 生成 UpToken - var authPolicy = new AuthPolicy(tabletName, expires); - string upToken = authPolicy.MakeAuthTokenString(); - -##### 2.2.2 使用UpToken上传文件 - - // 使用 UpToken 上传文件 - PutFileRet putFileRet = RSClient.PutFileWithUpToken( - upToken, tableName, key, mimeType, - filePath, customMeta, callbackParam); - - - -### 3. 获取已上传文件信息 - -您可以调用资源表对象的 Stat() 方法并传入一个 Key 来获取指定文件的相关信息。 - - // 获取资源表中特定文件信息 - StatRet statRet = rs.Stat(key); - -如果请求成功,得到的 statRet 将会包含如下几个字段: - - Hash: - FileSize: - PutTime: - MimeType: - - - -### 4. 下载文件 - -要下载一个文件,首先需要取得下载授权,所谓下载授权,就是取得一个临时合法有效的下载链接,只需传入相应的文件 Key 和下载要保存的文件名作为参数即可。 - - // 下载资源表中的特定文件 - GetRet getRet = rs.Get(key, filename); - GetRet getRet = rs.GetIfNotModified(key, filename, baseHash); - -返回的 getRet 包含如下字段: - - Url: # 获取文件内容的实际下载地址 - Hash: - FileSize: - MimeType: - -方法 GetIfNotModified 与 Get 的不同之处在于多了一个 baseHash 参数。这个参数是一个 SHA-1 值,用于判断所指向的文件内容是否被改动。只有在内容没有被变动时才会返回该文件的下载URL。 - - - -### 5. 发布公开资源 - -使用七牛云存储提供的资源发布功能,您可以将一个资源表里边的所有文件以静态链接可访问的方式公开发布到您自己的域名下。 - -要公开发布一个资源表里边的所有文件,只需调用该资源表对象的 Publish() 方法并传入域名作为参数即可。 - - // 公开发布某个资源表 - CallRet publishRet = rs.Publish(DomainName); - - - -### 6. 取消资源发布 - -调用资源表对象的 Unpublish() 方法可取消该资源表内所有文件的静态外链。 - - // 取消公开发布某个资源表 - CallRet unpublishRet = rs.Unpublish(DomainName); - - - -### 7. 删除已上传的文件 - -要删除指定的文件,只需调用资源表对象的 Delete() 方法并传入文件 key 作为参数即可。 - - // 删除资源表中的某个文件 - CallRet deleteRet = rs.Delete(key); - - - -### 8. 删除整张资源表 - -要删除整个资源表及该表里边的所有文件,可以调用资源表对象的 Drop() 方法。 -需慎重,这会删除整个表及其所有文件。 - - // 删除整个资源表 - CallRet dropRet = rs.Drop(); diff --git a/QBox/FileOp/FileOpClient.cs b/QBox/FileOp/FileOpClient.cs new file mode 100644 index 00000000..55fc1c5c --- /dev/null +++ b/QBox/FileOp/FileOpClient.cs @@ -0,0 +1,39 @@ +using System; +using System.Net; +using System.IO; +using QBox.RPC; + +namespace QBox.FileOp +{ + public static class FileOpClient + { + public static CallRet Get(string url) + { + Console.WriteLine("Client.Get ==> URL: " + url); + try + { + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); + request.Method = "GET"; + using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) + { + return HandleResult(response); + } + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + return new CallRet(HttpStatusCode.BadRequest, e); + } + } + + public static CallRet HandleResult(HttpWebResponse response) + { + HttpStatusCode statusCode = response.StatusCode; + using (StreamReader reader = new StreamReader(response.GetResponseStream())) + { + string responseStr = reader.ReadToEnd(); + return new CallRet(statusCode, responseStr); + } + } + } +} diff --git a/QBox/FileOp/ImageInfoRet.cs b/QBox/FileOp/ImageInfoRet.cs index 64b52ee9..d83afee5 100644 --- a/QBox/FileOp/ImageInfoRet.cs +++ b/QBox/FileOp/ImageInfoRet.cs @@ -1,6 +1,6 @@ using System; -using QBox.RS; using LitJson; +using QBox.RPC; namespace QBox.FileOp { diff --git a/QBox/FileOp/ImageOp.cs b/QBox/FileOp/ImageOp.cs index 02b83e3b..7fccb45f 100644 --- a/QBox/FileOp/ImageOp.cs +++ b/QBox/FileOp/ImageOp.cs @@ -1,34 +1,27 @@ using System; -using QBox.RS; +using QBox.RPC; namespace QBox.FileOp { - public class ImageOp + public static class ImageOp { - public Client Conn { get; private set; } - - public ImageOp(Client conn) - { - Conn = conn; - } - - public ImageInfoRet ImageInfo(string url) + public static ImageInfoRet ImageInfo(string url) { - CallRet callRet = Conn.Call(url + "?imageInfo"); + CallRet callRet = FileOpClient.Get(url + "?imageInfo"); return new ImageInfoRet(callRet); } - public CallRet ImageExif(string url) + public static CallRet ImageExif(string url) { - return Conn.Call(url + "?exif"); + return FileOpClient.Get(url + "?exif"); } - public string ImageViewUrl(string url, ImageViewSpec spec) + public static string ImageViewUrl(string url, ImageViewSpec spec) { return url + spec.MakeSpecString(); } - public string ImageMogrifyUrl(string url, ImageMogrifySpec spec) + public static string ImageMogrifyUrl(string url, ImageMogrifySpec spec) { return url + spec.MakeSpecString(); } diff --git a/QBox/QBox.csproj b/QBox/QBox.csproj index 86714eec..a71da270 100644 --- a/QBox/QBox.csproj +++ b/QBox/QBox.csproj @@ -38,6 +38,10 @@ + + + + @@ -53,18 +57,21 @@ - - - + + + + + + diff --git a/QBox/RS/CallRet.cs b/QBox/RPC/CallRet.cs similarity index 94% rename from QBox/RS/CallRet.cs rename to QBox/RPC/CallRet.cs index 540a1328..27f3624f 100644 --- a/QBox/RS/CallRet.cs +++ b/QBox/RPC/CallRet.cs @@ -1,7 +1,7 @@ using System; using System.Net; -namespace QBox.RS +namespace QBox.RPC { public class CallRet { diff --git a/QBox/RS/Client.cs b/QBox/RPC/Client.cs similarity index 82% rename from QBox/RS/Client.cs rename to QBox/RPC/Client.cs index 7c0ba38e..efc7950b 100644 --- a/QBox/RS/Client.cs +++ b/QBox/RPC/Client.cs @@ -1,16 +1,17 @@ using System; using System.Net; using System.IO; +using QBox.Util; -namespace QBox.RS +namespace QBox.RPC { public class Client { public virtual void SetAuth(HttpWebRequest request, Stream body) { } - + public CallRet Call(string url) { - Console.WriteLine("URL: " + url); + Console.WriteLine("Client.Post ==> URL: " + url); try { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); @@ -28,19 +29,19 @@ public CallRet Call(string url) } } - public CallRet CallWithBinary(string url, string contentType, Stream body) + public CallRet CallWithBinary(string url, string contentType, Stream body, long length) { - Console.WriteLine("URL: " + url); + Console.WriteLine("Client.PostWithBinary ==> URL: {0} Length:{1}", url, length); try { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = "POST"; request.ContentType = contentType; - request.ContentLength = body.Length; + request.ContentLength = length; SetAuth(request, body); using (Stream requestStream = request.GetRequestStream()) { - body.CopyTo(requestStream); + StreamUtil.CopyN(body, requestStream, length); } using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) { diff --git a/QBox/RS/Config.cs b/QBox/RS/Config.cs index b5936da2..f40d7fd3 100644 --- a/QBox/RS/Config.cs +++ b/QBox/RS/Config.cs @@ -10,5 +10,7 @@ public class Config public static string IO_HOST = "http://iovip.qbox.me"; public static string RS_HOST = "http://rs.qbox.me:10100"; public static string UP_HOST = "http://up.qbox.me"; + + public static int PUT_RETRY_TIMES = 3; } } diff --git a/QBox/RS/GetRet.cs b/QBox/RS/GetRet.cs index 16c978ff..b9b2bac2 100644 --- a/QBox/RS/GetRet.cs +++ b/QBox/RS/GetRet.cs @@ -1,6 +1,7 @@ using System; using System.Net; using LitJson; +using QBox.RPC; namespace QBox.RS { diff --git a/QBox/RS/MultiPartFormDataPost.cs b/QBox/RS/MultiPartFormDataPost.cs index 1acbf273..400f2c95 100644 --- a/QBox/RS/MultiPartFormDataPost.cs +++ b/QBox/RS/MultiPartFormDataPost.cs @@ -3,6 +3,8 @@ using System.Text; using System.IO; using System.Net; +using QBox.Util; +using QBox.RPC; namespace QBox.RS { @@ -71,7 +73,7 @@ private static void WriteBody(Dictionary postParams, string boun body.Write(encoding.GetBytes(header), 0, encoding.GetByteCount(header)); using (FileStream fs = File.OpenRead(fileToUpload.FileName)) { - fs.CopyTo(body); + StreamUtil.Copy(fs, body); } } else diff --git a/QBox/RS/PutAuthRet.cs b/QBox/RS/PutAuthRet.cs index 2158a1a1..0d4361b5 100644 --- a/QBox/RS/PutAuthRet.cs +++ b/QBox/RS/PutAuthRet.cs @@ -1,5 +1,6 @@ using System; using LitJson; +using QBox.RPC; namespace QBox.RS { diff --git a/QBox/RS/PutFileRet.cs b/QBox/RS/PutFileRet.cs index 64a5a0fa..eaba1389 100644 --- a/QBox/RS/PutFileRet.cs +++ b/QBox/RS/PutFileRet.cs @@ -1,5 +1,6 @@ using System; using LitJson; +using QBox.RPC; namespace QBox.RS { diff --git a/QBox/RS/RSClient.cs b/QBox/RS/RSClient.cs index a7a2ac10..67aa293c 100644 --- a/QBox/RS/RSClient.cs +++ b/QBox/RS/RSClient.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Net; +using QBox.Util; +using QBox.RPC; namespace QBox.RS { diff --git a/QBox/RS/RSService.cs b/QBox/RS/RSService.cs index 891649b9..dc40d1e8 100644 --- a/QBox/RS/RSService.cs +++ b/QBox/RS/RSService.cs @@ -2,6 +2,8 @@ using System.IO; using System.Net; using QBox.FileOp; +using QBox.Util; +using QBox.RPC; namespace QBox.RS { @@ -46,7 +48,7 @@ public PutFileRet PutFile(string key, string mimeType, string localFile, string { using (FileStream fs = File.OpenRead(localFile)) { - CallRet callRet = Conn.CallWithBinary(url, mimeType, fs); + CallRet callRet = Conn.CallWithBinary(url, mimeType, fs, fs.Length); return new PutFileRet(callRet); } } diff --git a/QBox/RS/ResumablePut.cs b/QBox/RS/ResumablePut.cs new file mode 100644 index 00000000..a74bfaa6 --- /dev/null +++ b/QBox/RS/ResumablePut.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using System.Text; +using QBox.Util; +using QBox.RPC; + +namespace QBox.RS +{ + public static class ResumablePut + { + private static int ChunkBits = 22; + private static long ChunkSize = 1 << ChunkBits; + + public static ResumablePutFileRet Mkblock(string host, Client client, Stream body, long length) + { + string url = host + "/mkblk/" + Convert.ToString(length); + CallRet callRet = client.CallWithBinary(url, "application/octet-stream", body, length); + return new ResumablePutFileRet(callRet); + } + + public static PutFileRet Mkfile(string host, Client client, string entryURI, long fsize, + string customMeta, string callbackParam, string[] ctxs) + { + string url = host + "/rs-mkfile/" + Base64UrlSafe.Encode(entryURI) + + "/fsize/" + Convert.ToString(fsize); + if (!String.IsNullOrEmpty(callbackParam)) + { + url += "/params/" + Base64UrlSafe.Encode(callbackParam); + } + if (!String.IsNullOrEmpty(customMeta)) + { + url += "/meta/" + Base64UrlSafe.Encode(customMeta); + } + + using (Stream body = new MemoryStream()) + { + for (int i = 0; i < ctxs.Length; i++) + { + byte[] bctx = Encoding.ASCII.GetBytes(ctxs[i]); + body.Write(bctx, 0, bctx.Length); + if (i != ctxs.Length-1) + { + body.WriteByte((byte)','); + } + } + body.Seek(0, SeekOrigin.Begin); + CallRet ret= client.CallWithBinary(url, "text/plain", body, body.Length); + return new PutFileRet(ret); + } + } + + public static PutFileRet PutFile( + Client client, string tableName, string key, string mimeType, + string localFile, string customMeta, string callbackParam) + { + long fsize = 0; + string[] ctxs = null; + string host = Config.UP_HOST; + using (FileStream fs = File.OpenRead(localFile)) + { + fsize = fs.Length; + int chunkCnt = (int)((fsize + (ChunkSize - 1)) >> ChunkBits); + long chunkSize = ChunkSize; + ctxs = new string[chunkCnt]; + Console.WriteLine("ResumablePut ==> fsize: {0}, chunkCnt: {1}", fsize, chunkCnt); + for (int i = 0; i < chunkCnt; i++) + { + if (i == chunkCnt - 1) + { + chunkSize = fsize - (i << ChunkBits); + } + ResumablePutFileRet ret = null; + for (int retry = 0; retry < Config.PUT_RETRY_TIMES; retry++) + { + fs.Seek(i * ChunkSize, SeekOrigin.Begin); + ret = Mkblock(host, client, fs, chunkSize); + if (ret.OK) + { + ctxs[i] = ret.Ctx; + host = ret.Host; + break; + } + } + if (!ret.OK) + { + Console.WriteLine(ret.Exception.ToString()); + return new PutFileRet(new CallRet(ret)); + } + } + } + + string entryURI = tableName + ":" + key; + return Mkfile(host, client, entryURI, fsize, customMeta, callbackParam, ctxs); + } + } +} diff --git a/QBox/RS/ResumablePutFileRet.cs b/QBox/RS/ResumablePutFileRet.cs new file mode 100644 index 00000000..5140712a --- /dev/null +++ b/QBox/RS/ResumablePutFileRet.cs @@ -0,0 +1,38 @@ +using System; +using LitJson; +using QBox.RPC; + +namespace QBox.RS +{ + public class ResumablePutFileRet : CallRet + { + public string Ctx { get; private set; } + public string Checksum { get; private set; } + public string Host { get; private set; } + + public ResumablePutFileRet(CallRet ret) + : base(ret) + { + if (!String.IsNullOrEmpty(Response)) + { + try + { + Unmarshal(Response); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + this.Exception = e; + } + } + } + + private void Unmarshal(string json) + { + JsonData data = JsonMapper.ToObject(json); + Ctx = (string)data["ctx"]; + Checksum = (string)data["checksum"]; + Host = (string)data["host"]; + } + } +} diff --git a/QBox/RS/StatRet.cs b/QBox/RS/StatRet.cs index 89b91647..0ce1c12d 100644 --- a/QBox/RS/StatRet.cs +++ b/QBox/RS/StatRet.cs @@ -1,5 +1,6 @@ using System; using LitJson; +using QBox.RPC; namespace QBox.RS { diff --git a/QBox/RS/Base64UrlSafe.cs b/QBox/Util/Base64UrlSafe.cs similarity index 87% rename from QBox/RS/Base64UrlSafe.cs rename to QBox/Util/Base64UrlSafe.cs index 3d603cbd..5e242937 100644 --- a/QBox/RS/Base64UrlSafe.cs +++ b/QBox/Util/Base64UrlSafe.cs @@ -1,14 +1,14 @@ using System; using System.Text; -namespace QBox.RS +namespace QBox.Util { public static class Base64UrlSafe { public static string Encode(string text) { if (String.IsNullOrEmpty(text)) return ""; - byte[] bs = Encoding.ASCII.GetBytes(text); + byte[] bs = Encoding.UTF8.GetBytes(text); string encodedStr = Convert.ToBase64String(bs); encodedStr = encodedStr.Replace('+', '-').Replace('/', '_'); return encodedStr; diff --git a/QBox/Util/StreamUtil.cs b/QBox/Util/StreamUtil.cs new file mode 100644 index 00000000..1a94a85d --- /dev/null +++ b/QBox/Util/StreamUtil.cs @@ -0,0 +1,46 @@ +using System; +using System.Text; +using System.IO; + +namespace QBox.Util +{ + public static class StreamUtil + { + public static int bufferLen = 32*1024; + + public static void Copy(Stream src, Stream dst) + { + byte[] buffer = new byte[bufferLen]; + while (true) + { + int n = src.Read(buffer, 0, bufferLen); + if (n == 0) break; + dst.Write(buffer, 0, n); + } + } + + public static void CopyN(Stream src, Stream dst, long numBytesToCopy) + { + Console.WriteLine("Stream.CopyN: {0}", numBytesToCopy); + byte[] buffer = new byte[bufferLen]; + long numBytesWritten = 0; + while (numBytesWritten < numBytesToCopy) + { + int len = bufferLen; + if ((numBytesToCopy - numBytesWritten) < len) + { + len = (int)(numBytesToCopy - numBytesWritten); + } + int n = src.Read(buffer, 0, len); + if (n == 0) break; + dst.Write(buffer, 0, n); + numBytesWritten += n; + //Console.WriteLine("Stream.CopyN.Write: {0} {1}", n, numBytesWritten); + } + if (numBytesWritten != numBytesToCopy) + { + throw new Exception("StreamUtil.CopyN: nwritten not equal to ncopy"); + } + } + } +}