├── .gitignore
├── demo
├── jpegToBase64.cs
├── Properties
│ └── AssemblyInfo.cs
├── demo.csproj
├── Program.cs
└── Base64File.cs
├── sdk
├── Properties
│ └── AssemblyInfo.cs
├── QiniuUtils.cs
├── QiniuException.cs
├── QiniuConf.cs
├── QiniuErrors.cs
├── qiniu.csproj
├── QiniuMAC.cs
├── QiniuResumbleUploadEx.cs
├── QiniuEventArgs.cs
├── QiniuPutPolicy.cs
├── QiniuWebClient.cs
└── QiniuFile.cs
├── LICENSE
├── qiniu-csharp-sdk.sln
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # Object files
2 | *.o
3 | *.ko
4 | *.obj
5 | *.elf
6 |
7 | # Libraries
8 | *.lib
9 | *.a
10 |
11 | # Shared objects (inc. Windows DLLs)
12 | *.dll
13 | *.so
14 | *.so.*
15 | *.dylib
16 |
17 | # Executables
18 | *.exe
19 | *.out
20 | *.app
21 | *.i*86
22 | *.x86_64
23 | *.hex
24 |
25 | obj
26 | bin
27 | qiniu-csharp-sdk.userprefs
28 |
--------------------------------------------------------------------------------
/demo/jpegToBase64.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace demo
5 | {
6 | public class jpegToBase64
7 | {
8 | string filename;
9 | long filesize;
10 | string base64Content;
11 |
12 | public string Filename {
13 | get { return filename; }
14 | }
15 |
16 | public long Filesize {
17 | get { return filesize; }
18 | }
19 |
20 | public string Base64Content {
21 | get { return base64Content; }
22 | }
23 |
24 | public jpegToBase64 (string filename)
25 | {
26 | if (!File.Exists (filename)) {
27 | throw new ArgumentException (filename + " not exists");
28 | }
29 | this.filename = filename;
30 | FileInfo finfo = new FileInfo (filename);
31 | this.filesize = finfo.Length;
32 | using (FileStream fs = finfo.OpenRead ()) {
33 | byte[] content = new byte[this.filesize];
34 | fs.Read (content, 0, (int)this.filesize);
35 | this.base64Content = Convert.ToBase64String (content);
36 | }
37 | }
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/demo/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 |
4 | // Information about this assembly is defined by the following attributes.
5 | // Change them to the values specific to your project.
6 | [assembly: AssemblyTitle ("demo")]
7 | [assembly: AssemblyDescription ("")]
8 | [assembly: AssemblyConfiguration ("")]
9 | [assembly: AssemblyCompany ("")]
10 | [assembly: AssemblyProduct ("")]
11 | [assembly: AssemblyCopyright ("icattlecoder")]
12 | [assembly: AssemblyTrademark ("")]
13 | [assembly: AssemblyCulture ("")]
14 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
15 | // The form "{Major}.{Minor}.*" will automatically update the build and revision,
16 | // and "{Major}.{Minor}.{Build}.*" will update just the revision.
17 | [assembly: AssemblyVersion ("1.0.*")]
18 | // The following attributes are used to specify the signing key for the assembly,
19 | // if desired. See the Mono documentation for more information about signing.
20 | //[assembly: AssemblyDelaySign(false)]
21 | //[assembly: AssemblyKeyFile("")]
22 |
23 |
--------------------------------------------------------------------------------
/sdk/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 |
4 | // Information about this assembly is defined by the following attributes.
5 | // Change them to the values specific to your project.
6 | [assembly: AssemblyTitle ("qiniu-csharp-sdk")]
7 | [assembly: AssemblyDescription ("")]
8 | [assembly: AssemblyConfiguration ("")]
9 | [assembly: AssemblyCompany ("")]
10 | [assembly: AssemblyProduct ("")]
11 | [assembly: AssemblyCopyright ("icattlecoder")]
12 | [assembly: AssemblyTrademark ("")]
13 | [assembly: AssemblyCulture ("")]
14 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
15 | // The form "{Major}.{Minor}.*" will automatically update the build and revision,
16 | // and "{Major}.{Minor}.{Build}.*" will update just the revision.
17 | [assembly: AssemblyVersion ("1.0.*")]
18 | // The following attributes are used to specify the signing key for the assembly,
19 | // if desired. See the Mono documentation for more information about signing.
20 | //[assembly: AssemblyDelaySign(false)]
21 | //[assembly: AssemblyKeyFile("")]
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 wangming
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/sdk/QiniuUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Encoding = System.Text.UTF8Encoding;
3 |
4 | namespace qiniu
5 | {
6 | public static class Base64URLSafe
7 | {
8 | public static string Encode (string text)
9 | {
10 | if (String.IsNullOrEmpty (text))
11 | return "";
12 | byte[] bs = Encoding.UTF8.GetBytes (text);
13 | string encodedStr = Convert.ToBase64String (bs);
14 | encodedStr = encodedStr.Replace ('+', '-').Replace ('/', '_');
15 | return encodedStr;
16 | }
17 |
18 | ///
19 | /// To the base64 safe URL.
20 | ///
21 | /// The base64 URL safe.
22 | /// String.
23 | public static string ToBase64URLSafe (string str)
24 | {
25 | return Encode (str);
26 | }
27 |
28 | ///
29 | /// Encode the specified bs.
30 | ///
31 | /// Bs.
32 | public static string Encode (byte[] bs)
33 | {
34 | if (bs == null || bs.Length == 0)
35 | return "";
36 | string encodedStr = Convert.ToBase64String (bs);
37 | encodedStr = encodedStr.Replace ('+', '-').Replace ('/', '_');
38 | return encodedStr;
39 | }
40 | }
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/sdk/QiniuException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 |
4 | namespace qiniu
5 | {
6 | ///
7 | /// Qiniu web exception.
8 | ///
9 | public class QiniuWebException:Exception
10 | {
11 |
12 | #region
13 | private QiniuErrors error;
14 | private string x_Reqid;
15 | private string x_Log;
16 | #endregion
17 |
18 | ///
19 | /// Gets the x_reqid.
20 | ///
21 | /// The x_reqid.
22 | public string X_Reqid {
23 | get { return x_Reqid; }
24 | }
25 |
26 | ///
27 | /// Gets the x_log.
28 | ///
29 | /// The x_log.
30 | public string X_Log {
31 | get { return x_Log; }
32 | }
33 |
34 | ///
35 | /// Gets the error.
36 | ///
37 | /// The error.
38 | public QiniuErrors Error {
39 | get { return error; }
40 | }
41 |
42 | ///
43 | /// Initializes a new instance of the class.
44 | ///
45 | /// We.
46 | public QiniuWebException (WebException we) {
47 | if (we.Response is HttpWebResponse) {
48 | HttpWebResponse hwr = we.Response as HttpWebResponse;
49 | this.error = new QiniuErrors ((int)hwr.StatusCode);
50 | this.x_Reqid = hwr.Headers ["X-Reqid"];
51 | this.x_Log = hwr.Headers ["X-Log"];
52 | }
53 | }
54 | }
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/sdk/QiniuConf.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace qiniu
5 | {
6 | ///
7 | /// Config.
8 | ///
9 | public class Config
10 | {
11 | public static string USER_AGENT = "qiniu-csharp-sdk icattlecoder v1.0.0";
12 | #region 帐户信息
13 | ///
14 | /// 七牛提供的公钥,用于识别用户
15 | ///
16 | public static string ACCESS_KEY = "";
17 | ///
18 | /// 七牛提供的秘钥,不要在客户端初始化该变量
19 | ///
20 | public static string SECRET_KEY = "";
21 | #endregion
22 |
23 | #region 七牛服务器地址
24 | ///
25 | /// 七牛资源管理服务器地址
26 | ///
27 | public static string RS_HOST = "http://rs.Qbox.me";
28 | ///
29 | /// 七牛资源上传服务器地址.
30 | ///
31 | public static string UP_HOST = "http://up.qiniu.com";
32 | ///
33 | /// 七牛资源列表服务器地址.
34 | ///
35 | public static string RSF_HOST = "http://rsf.Qbox.me";
36 |
37 | public static string PREFETCH_HOST = "http://iovip.qbox.me";
38 |
39 | public static string API_HOST = "http://api.qiniu.com";
40 | #endregion
41 |
42 | ///
43 | /// 初始化七牛帐户、请求地址等信息,不应在客户端调用。
44 | ///
45 | public static void InitFromAppConfig()
46 | {
47 | ACCESS_KEY = System.Configuration.ConfigurationManager.AppSettings["ACCESS_KEY"];
48 | SECRET_KEY = System.Configuration.ConfigurationManager.AppSettings["SECRET_KEY"];
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/qiniu-csharp-sdk.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 11.00
3 | # Visual Studio 2010
4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "qiniu", "sdk\qiniu.csproj", "{8D120ACA-4BF9-42A7-81C6-8BE39FF5ED42}"
5 | EndProject
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "demo", "demo\demo.csproj", "{4F38040D-651B-4273-AE85-D96BC59C4698}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {4F38040D-651B-4273-AE85-D96BC59C4698}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {4F38040D-651B-4273-AE85-D96BC59C4698}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {4F38040D-651B-4273-AE85-D96BC59C4698}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {4F38040D-651B-4273-AE85-D96BC59C4698}.Release|Any CPU.Build.0 = Release|Any CPU
18 | {8D120ACA-4BF9-42A7-81C6-8BE39FF5ED42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {8D120ACA-4BF9-42A7-81C6-8BE39FF5ED42}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {8D120ACA-4BF9-42A7-81C6-8BE39FF5ED42}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {8D120ACA-4BF9-42A7-81C6-8BE39FF5ED42}.Release|Any CPU.Build.0 = Release|Any CPU
22 | EndGlobalSection
23 | GlobalSection(MonoDevelopProperties) = preSolution
24 | StartupItem = demo\demo.csproj
25 | EndGlobalSection
26 | EndGlobal
27 |
--------------------------------------------------------------------------------
/sdk/QiniuErrors.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace qiniu
5 | {
6 | public class QiniuErrors
7 | {
8 | ///
9 | /// See http://developer.qiniu.com/docs/v6/api/reference/codes.html
10 | ///
11 | public static Dictionary ErrorCodes = new Dictionary {
12 | {298,"部分操作执行成功"},
13 | {400,"请求报文格式错误"},
14 | {401,"认证授权失败,可能是密钥信息不正确、数字签名错误或授权已超时"},
15 | {404,"资源不存在"},
16 | {405,"请求方式错误,非预期的请求方式"},
17 | {406,"上传的数据 CRC32 校验错"},
18 | {419,"用户账号被冻结"},
19 | {503,"服务端不可用"},
20 | {504,"服务端操作超时"},
21 | {579,"资源上传成功,但是回调失败"},
22 | {599,"服务端操作失败"},
23 | {608,"资源内容被修改"},
24 | {612,"指定资源不存在或已被删除"},
25 | {614,"目标资源已存在"},
26 | {630,"已创建的空间数量达到上限,无法创建新空间"},
27 | {631,"指定空间不存在"},
28 | {701,"上传数据块校验出错"}
29 | };
30 |
31 | private int httpCode;
32 |
33 | ///
34 | /// Gets the http code.
35 | ///
36 | /// The http code.
37 | public int HttpCode {
38 | get { return httpCode; }
39 | }
40 |
41 | ///
42 | /// Initializes a new instance of the class.
43 | ///
44 | /// Code.
45 | public QiniuErrors (int code)
46 | {
47 | this.httpCode = code;
48 | }
49 |
50 | ///
51 | /// Returns a that represents the current .
52 | ///
53 | /// A that represents the current .
54 | public override string ToString ()
55 | {
56 | return ErrorCodes [this.httpCode];
57 | }
58 | }
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/demo/demo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | 10.0.0
7 | 2.0
8 | {4F38040D-651B-4273-AE85-D96BC59C4698}
9 | Exe
10 | demo
11 | demo
12 |
13 |
14 | true
15 | full
16 | false
17 | bin\Debug
18 | DEBUG;
19 | prompt
20 | 4
21 | true
22 |
23 |
24 | full
25 | true
26 | bin\Release
27 | prompt
28 | 4
29 | true
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {8D120ACA-4BF9-42A7-81C6-8BE39FF5ED42}
44 | qiniu
45 |
46 |
47 |
--------------------------------------------------------------------------------
/sdk/qiniu.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | 10.0.0
7 | 2.0
8 | {8D120ACA-4BF9-42A7-81C6-8BE39FF5ED42}
9 | Library
10 | qiniucsharpsdk
11 | qiniu-csharp-sdk
12 |
13 |
14 | true
15 | full
16 | false
17 | bin\Debug
18 | DEBUG;
19 | prompt
20 | 4
21 | false
22 |
23 |
24 | full
25 | true
26 | bin\Release
27 | prompt
28 | 4
29 | false
30 |
31 |
32 |
33 |
34 |
35 |
36 | bin\Debug\Newtonsoft.Json.dll
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/sdk/QiniuMAC.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Security.Cryptography;
4 | using Encoding = System.Text.UTF8Encoding;
5 |
6 | namespace qiniu
7 | {
8 | public class MAC
9 | {
10 |
11 | private string accessKey;
12 | private byte[] secretKey;
13 |
14 | ///
15 | /// Gets or sets the access key.
16 | ///
17 | /// The access key.
18 | public string AccessKey {
19 | get { return accessKey; }
20 | set { accessKey = value; }
21 | }
22 |
23 | ///
24 | /// Gets or sets the secret key.
25 | ///
26 | /// The secret key.
27 | public string SecretKey {
28 | set { secretKey = Encoding.UTF8.GetBytes(value); }
29 | }
30 |
31 | ///
32 | /// Initializes a new instance of the class.
33 | ///
34 | /// Access key.
35 | /// Secret key.
36 | public MAC (string accessKey,string secretKey )
37 | {
38 | this.accessKey = accessKey;
39 | SecretKey = secretKey;
40 | }
41 |
42 | ///
43 | /// Initializes a new instance of the class.
44 | /// Get accesskey and secretKey from the Config
45 | ///
46 | public MAC()
47 | {
48 | this.accessKey = Config.ACCESS_KEY;
49 | SecretKey = Config.SECRET_KEY;
50 | }
51 |
52 | private string _sign (byte[] data)
53 | {
54 | HMACSHA1 hmac = new HMACSHA1 (this.secretKey);
55 | byte[] digest = hmac.ComputeHash (data);
56 | return Base64URLSafe.Encode (digest);
57 | }
58 |
59 | ///
60 | /// Sign
61 | ///
62 | ///
63 | ///
64 | public string Sign (byte[] b)
65 | {
66 | return string.Format ("{0}:{1}", this.accessKey, _sign (b));
67 | }
68 |
69 |
70 | ///
71 | /// SignWithData
72 | ///
73 | ///
74 | ///
75 | public string SignWithData (byte[] b)
76 | {
77 | string data = Base64URLSafe.Encode (b);
78 | return string.Format ("{0}:{1}:{2}", this.accessKey, _sign (Encoding.UTF8.GetBytes (data)), data);
79 | }
80 |
81 | ///
82 | /// SignRequest
83 | ///
84 | ///
85 | ///
86 | ///
87 | public void SignRequest (System.Net.HttpWebRequest request, byte[] body)
88 | {
89 | Uri u = request.Address;
90 | using (HMACSHA1 hmac = new HMACSHA1(secretKey)) {
91 | string pathAndQuery = request.Address.PathAndQuery;
92 | byte[] pathAndQueryBytes = Encoding.UTF8.GetBytes (pathAndQuery);
93 | using (MemoryStream buffer = new MemoryStream()) {
94 | buffer.Write (pathAndQueryBytes, 0, pathAndQueryBytes.Length);
95 | buffer.WriteByte ((byte)'\n');
96 | if (body!=null&&body.Length > 0) {
97 | buffer.Write (body, 0, body.Length);
98 | }
99 | byte[] digest = hmac.ComputeHash (buffer.ToArray ());
100 | string digestBase64 = Base64URLSafe.Encode (digest);
101 | request.Headers.Add ("Authorization", "QBox " + this.accessKey+":"+digestBase64);
102 | }
103 | }
104 | }
105 |
106 | }
107 | }
108 |
109 |
--------------------------------------------------------------------------------
/demo/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using qiniu;
5 | using System.Threading;
6 |
7 | namespace demo
8 | {
9 | class MainClass
10 | {
11 | public static void Main (string[] args)
12 | {
13 | // 初始化qiniu配置,主要是API Keys
14 | qiniu.Config.ACCESS_KEY = "IT9iP3J9wdXXYsT1p8ns0gWD-CQOdLvIQuyE0FOk";
15 | qiniu.Config.SECRET_KEY = "zUCzekBtEqTZ4-WJPCGlBrr2PeyYxsYn98LPaivM";
16 |
17 | /**********************************************************************
18 | 可以用下面的方法从配置文件中初始化
19 | qiniu.Config.InitFromAppConfig ();
20 | **********************************************************************/
21 |
22 | string localfile = "/Users/icattlecoder/Movies/tzd.rmvb";
23 | string bucket = "icattlecoder";
24 | string qiniukey = "tzd.rmvb";
25 |
26 | {
27 | Base64File bfile = new Base64File (Base64File.TESTBASE64);
28 | bfile.Save ("/Users/icattlecoder/Desktop/dbase64.jpg");
29 | }
30 |
31 | {
32 | UploadBase642 ();
33 | }
34 |
35 | {
36 | UploadBase64 ();
37 | }
38 |
39 | //======================================================================
40 | {
41 | QiniuFile qfile = new QiniuFile (bucket, qiniukey, localfile);
42 | // ResumbleUploadEx puttedCtx = new ResumbleUploadEx (localfile);
43 | ManualResetEvent done = new ManualResetEvent (false);
44 | qfile.UploadCompleted += (sender, e) => {
45 | Console.WriteLine (e.key);
46 | Console.WriteLine (e.Hash);
47 | done.Set ();
48 | };
49 | qfile.UploadFailed += (sender, e) => {
50 | Console.WriteLine (e.Error.ToString ());
51 | // puttedCtx.Save();
52 | done.Set ();
53 | };
54 |
55 | qfile.UploadProgressChanged += (sender, e) => {
56 | int percentage = (int)(100 * e.BytesSent / e.TotalBytes);
57 | Console.Write (percentage);
58 | };
59 | qfile.UploadBlockCompleted += (sender, e) => {
60 | // puttedCtx.Add(e.Index,e.Ctx);
61 | // puttedCtx.Save();
62 | };
63 | qfile.UploadBlockFailed += (sender, e) => {
64 | //
65 |
66 | };
67 |
68 | //上传为异步操作
69 | //上传本地文件到七牛云存储
70 | // qfile.Upload (puttedCtx.PuttedCtx);
71 | qfile.Upload ();
72 | done.WaitOne ();
73 | }
74 |
75 | //======================================================================
76 | {
77 | /*
78 |
79 | try {
80 | QiniuFile qfile = new QiniuFile (bucket, qiniukey);
81 | QiniuFileInfo finfo = qfile.Stat ();
82 | if (finfo != null) {
83 | qfile.Move("cloudcomment","movetest");
84 | //删除七牛云空间的文件
85 | //qfile.Delete ();
86 | }
87 | } catch (QiniuWebException e) {
88 | Console.WriteLine (e.Error.HttpCode);
89 | Console.WriteLine (e.Error.ToString ());
90 | }
91 | */
92 | }
93 | }
94 |
95 | ///
96 | /// Uploads the base64.
97 | ///
98 | public static void UploadBase64(){
99 | string bucket = "icattlecoder";
100 | string qiniuKey = "base64.png";
101 |
102 | ManualResetEvent done = new ManualResetEvent (false);
103 | jpegToBase64 jpeg = new jpegToBase64 ("/Users/icattlecoder/Desktop/base64.png");
104 | QiniuFile qfile = new QiniuFile (bucket, qiniuKey);
105 | qfile.UploadCompleted+= (sender, e) => {
106 | Console.Write(e.RawString);
107 | done.Set();
108 |
109 | };
110 | qfile.UploadFailed+= (sender, e) => {
111 | QiniuWebException qe = (QiniuWebException)e.Error;
112 | Console.WriteLine(qe.Error.ToString());
113 | };
114 | qfile.UploadString ((int)jpeg.Filesize, "image/png", jpeg.Base64Content);
115 | done.WaitOne ();
116 | }
117 |
118 | public static void UploadBase642(){
119 | string bucket = "icattlecoder";
120 | string qiniuKey = "base642.png";
121 |
122 | ManualResetEvent done = new ManualResetEvent (false);
123 | Base64File bfile = new Base64File (Base64File.TESTBASE64);
124 |
125 |
126 | QiniuFile qfile = new QiniuFile (bucket, qiniuKey);
127 | qfile.UploadCompleted+= (sender, e) => {
128 | Console.Write(e.RawString);
129 | done.Set();
130 |
131 | };
132 | qfile.UploadFailed+= (sender, e) => {
133 | QiniuWebException qe = (QiniuWebException)e.Error;
134 | Console.WriteLine(qe.Error.ToString());
135 | };
136 | qfile.UploadString ((int)bfile.FileSize, "image/png", Base64File.TESTBASE64);
137 | done.WaitOne ();
138 |
139 | }
140 |
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/sdk/QiniuResumbleUploadEx.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using System.Security.Cryptography;
5 | using System.Collections.Generic;
6 |
7 | namespace qiniu
8 | {
9 | ///
10 | /// 断点续传.
11 | ///
12 | public class ResumbleUploadEx
13 | {
14 | private string puttedCtxDir;
15 | private string fileName;
16 | private string puttedCtxFileName;
17 | private Dictionary puttedCtx;
18 |
19 | ///
20 | /// 获取已上传的结果.
21 | ///
22 | /// The putted context.
23 | public BlkputRet[] PuttedCtx {
24 | get {
25 | if (this.puttedCtx == null || this.puttedCtx.Count == 0)
26 | return null;
27 | BlkputRet[] result = new BlkputRet[this.puttedCtx.Count];
28 | for (int i = 0; i < this.puttedCtx.Count; i++) {
29 | if (this.puttedCtx.ContainsKey (i)) {
30 | result [i] = this.puttedCtx [i];
31 | } else {
32 | this.Clear ();
33 | return null;
34 | }
35 | }
36 | return result;
37 | }
38 | }
39 |
40 | ///
41 | /// Initializes a new instance of the class.
42 | ///
43 | /// Filename.
44 | /// 断点续传上传结果持久化存放目录. 默认存放在系统临时文件夹。
45 | public ResumbleUploadEx (string filename, string puttedCtxDir=null){
46 | if (!File.Exists (filename)) {
47 | throw new Exception(string.Format("{0} does not exist", filename));
48 | }
49 | this.fileName = filename;
50 | if (puttedCtxDir != null) {
51 | if (!Directory.Exists (puttedCtxDir)) {
52 | try{
53 | Directory.CreateDirectory (puttedCtxDir);
54 | }
55 | catch(Exception e){
56 | throw e;
57 | }
58 | }
59 | this.puttedCtxDir = puttedCtxDir;
60 | } else {
61 | this.puttedCtxDir = Path.GetTempPath ();
62 | }
63 | string ctxfile = getFileBase64Sha1 (this.fileName);
64 | this.puttedCtxFileName = Path.Combine (this.puttedCtxDir, ctxfile);
65 | if (!File.Exists (this.puttedCtxFileName)) {
66 | File.Open (this.puttedCtxFileName, FileMode.Create);
67 | this.puttedCtx = new Dictionary ();
68 | } else {
69 | this.puttedCtx = initloadPuttedCtx ();
70 | }
71 | }
72 |
73 | private Dictionary initloadPuttedCtx(){
74 |
75 | Dictionary result = new Dictionary ();
76 | string[] lines = File.ReadAllLines(this.puttedCtxFileName);
77 | foreach (string line in lines)
78 | {
79 | string[] fields = line.Split(',');
80 | BlkputRet ret = new BlkputRet();
81 | ret.offset = ulong.Parse(fields[1]);
82 | ret.ctx = fields[2];
83 | int idx = int.Parse(fields[0]);
84 | result.Add (idx, ret);
85 | }
86 | return result;
87 | }
88 |
89 | ///
90 | /// 保存持久化结果至磁盘文件
91 | ///
92 | public void Save(){
93 | StringBuilder sb = new StringBuilder ();
94 | foreach (int i in this.puttedCtx.Keys) {
95 | string content = i + "," + this.puttedCtx[i].offset + "," + this.puttedCtx[i].ctx + "\n";
96 | sb.Append (content);
97 | }
98 | File.WriteAllText (this.puttedCtxFileName, sb.ToString ());
99 | }
100 |
101 | ///
102 | /// 保存持久化结果至内存
103 | ///
104 | ///
105 | ///
106 | ///
107 | public void Add(int idx, BlkputRet ret)
108 | {
109 | this.puttedCtx [idx] = ret;
110 | }
111 |
112 | ///
113 | /// 保存持久化结果至内存和文件.
114 | ///
115 | /// Index.
116 | /// Ret.
117 | public void AddAndSave(int idx,BlkputRet ret){
118 | this.Add (idx, ret);
119 | this.Save ();
120 | }
121 |
122 | ///
123 | /// 清除上传持久化结果
124 | ///
125 | public void Clear(){
126 | if (File.Exists (this.puttedCtxFileName)) {
127 | this.puttedCtx.Clear ();
128 | File.Delete (this.puttedCtxFileName);
129 | }
130 | }
131 |
132 | ///
133 | /// 获取文件的SHA1值
134 | ///
135 | ///
136 | /// base64编码的sha1值
137 | private static string getFileBase64Sha1(string filename)
138 | {
139 | SHA1 sha1 = new SHA1CryptoServiceProvider();
140 | using (Stream reader = System.IO.File.OpenRead(filename))
141 | {
142 | byte[] result = sha1.ComputeHash(reader);
143 | return BitConverter.ToString(result);
144 | }
145 | }
146 | }
147 | }
148 |
149 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | qiniu-csharp-sdk
2 | ================
3 |
4 | 七牛云存储SDK
5 |
6 | 此SDK实现了七牛云存储的核心部分,即文件上传,目的是简化文件上传并提供更加便捷的编程接口,从更高层面进行了抽象,而非官方SDK那样,仅是对API的一套封装。
7 |
8 | 项目地址: [https://github.com/icattlecoder/qiniu-csharp-sdk](https://github.com/icattlecoder/qiniu-csharp-sdk)
9 |
10 | - [初始化](#init)
11 | - [上传文件](#upload)
12 | - [上传事件](#event)
13 | - [上传结果](#result)
14 | - [续传](#resumble)
15 | - [文件操作](#rsop)
16 | - [查看信息](#stat)
17 | - [删除](#delete)
18 | - [问题反馈](#issue)
19 |
20 |
21 | # 初始化
22 |
23 | 初始化工作包括对七牛的API Keys的赋值,如:
24 | ```c#
25 | qiniu.Config.ACCESS_KEY = "IT9iP3J9wdXXYsT1p8ns0gWD-CQOdLvIQuyE0FOK";
26 | qiniu.Config.SECRET_KEY = "zUCzekBtEqTZ4-WJPCGlBrr2PeyYxsYn98LPaivM";
27 | ```
28 |
29 | # 上传文件
30 |
31 | ```c#
32 | QiniuFile qfile = new QiniuFile ("", "", "");
33 | qfile.Upload();
34 | ```
35 |
36 | 一个QiniuFile对象表示一个七牛云空间的文件,初始化QiniuFile提供以下三个参数:
37 | - `bucketName`,七牛云空间名称
38 | - `key`,七牛文件key
39 | - `localfile`,本地文件。该参数为可选,如果要上传本地的文件至七牛云空间,则需要指定此参数的值。
40 |
41 | 注意上传为异步操作,上传的结果由事件通知。
42 |
43 |
44 | ## 上传事件
45 |
46 | 共包括五个事件
47 |
48 | 事件名称 | 说明
49 | :--------- | :---------
50 | UploadCompleted; | 上传完成
51 | UploadFailed; | 上传失败
52 | UploadProgressChanged; | 上传进度
53 | UploadBlockCompleted; | 上传块完成
54 | UploadBlockFailed; | 上传块失败
55 |
56 | 前三个事件比较容易理解,后两个事件是根据七牛的大文件上传机制衍生出来的,合理利用这两个事件可以完成大文件分块上传结果持久化,从而实现续传。
57 |
58 |
59 | ## 上传结果
60 | 成功上传一个文件后,结果通过事件`uploadCompleted`获取得到,包括文件的`Hash`和`Key`以及从七牛云存储返回的原始字符串(主要考虑到上传凭证中指定了自定义的returnBody)。
61 |
62 | ## 续传
63 |
64 | 类`QiniuResumbleUploadEx`可用于续传,见示例。
65 |
66 |
67 | ## 文件操作
68 | 简单的实现了文件的基本信息获取及删除操作,分别为`Stat`和`Delete`
69 |
70 | # 完整示例
71 |
72 | ```c#
73 | using System;
74 | using System.Collections;
75 | using System.Collections.Generic;
76 | using qiniu;
77 | using System.Threading;
78 |
79 | namespace demo
80 | {
81 | class MainClass
82 | {
83 | public static void Main (string[] args)
84 | {
85 | // 初始化qiniu配置,主要是API Keys
86 | qiniu.Config.ACCESS_KEY = "IT9iP3J9wdXXYsT1p8ns0gWD-CQOdLvIQuyE0FOi";
87 | qiniu.Config.SECRET_KEY = "zUCzekBtEqTZ4-WJPCGlBrr2PeyYxsYn98LPaivM";
88 |
89 | /**********************************************************************
90 | 可以用下面的方法从配置文件中初始化
91 | qiniu.Config.InitFromAppConfig ();
92 | **********************************************************************/
93 |
94 | string localfile = "/Users/icattlecoder/Movies/tzd.rmvb";
95 | string bucket = "icattlecoder";
96 | string qiniukey = "tzd.rmvb";
97 |
98 | //======================================================================
99 | {
100 | QiniuFile qfile = new QiniuFile (bucket, qiniukey, localfile);
101 |
102 | ResumbleUploadEx puttedCtx = new ResumbleUploadEx (localfile); //续传
103 |
104 | ManualResetEvent done = new ManualResetEvent (false);
105 | qfile.UploadCompleted += (sender, e) => {
106 | Console.WriteLine (e.key);
107 | Console.WriteLine (e.Hash);
108 | done.Set ();
109 | };
110 | qfile.UploadFailed += (sender, e) => {
111 | Console.WriteLine (e.Error.ToString ());
112 | puttedCtx.Save();
113 | done.Set ();
114 | };
115 | qfile.UploadProgressChanged += (sender, e) => {
116 | int percentage = (int)(100 * e.BytesSent / e.TotalBytes);
117 | Console.Write (percentage);
118 | };
119 | qfile.UploadBlockCompleted += (sender, e) => {
120 | //上传结果持久化
121 | puttedCtx.Add(e.Index,e.Ctx);
122 | puttedCtx.Save();
123 | };
124 | qfile.UploadBlockFailed += (sender, e) => {
125 | //
126 | };
127 |
128 | //上传为异步操作
129 | //上传本地文件到七牛云存储
130 | qfile.Upload ();
131 |
132 | //如果要续传,调用下面的方法
133 | //qfile.Upload (puttedCtx.PuttedCtx);
134 |
135 | done.WaitOne ();
136 | }
137 |
138 | //======================================================================
139 | {
140 |
141 | try {
142 | QiniuFile qfile = new QiniuFile (bucket, qiniukey);
143 | QiniuFileInfo finfo = qfile.Stat ();
144 | if (finfo != null) {
145 | qfile.Move("cloudcomment","movetest");
146 | //删除七牛云空间的文件
147 | //qfile.Delete ();
148 | }
149 | } catch (QiniuWebException e) {
150 | Console.WriteLine (e.Error.HttpCode);
151 | Console.WriteLine (e.Error.ToString ());
152 | }
153 | }
154 | }
155 | }
156 | }
157 |
158 | ```
159 |
160 |
161 | # 问题反馈
162 | [https://github.com/icattlecoder/qiniu-csharp-sdk/issues/new](https://github.com/icattlecoder/qiniu-csharp-sdk/issues/new)
163 |
164 |
--------------------------------------------------------------------------------
/sdk/QiniuEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Newtonsoft.Json;
4 | using Encoding = System.Text.UTF8Encoding;
5 | namespace qiniu
6 | {
7 | ///
8 | /// Qiniu upload failed event arguments.
9 | ///
10 | public class QiniuUploadFailedEventArgs: EventArgs
11 | {
12 | private Exception error;
13 |
14 | ///
15 | /// Gets the error.
16 | ///
17 | /// The error.
18 | public Exception Error {
19 | get { return error; }
20 | }
21 |
22 | ///
23 | /// Initializes a new instance of the class.
24 | ///
25 | /// E.
26 | public QiniuUploadFailedEventArgs(Exception e){
27 | this.error = e;
28 | }
29 | }
30 |
31 | ///
32 | /// Qiniu upload completed event arguments.
33 | ///
34 | public class QiniuUploadCompletedEventArgs:EventArgs{
35 |
36 | ///
37 | /// 上传结果的原始JOSN字符串。
38 | ///
39 | /// The raw string.
40 | public string RawString { get; private set;}
41 |
42 | ///
43 | /// 如果 uptoken 没有指定 ReturnBody,那么返回值是标准的 PutRet 结构
44 | ///
45 | public string Hash { get; private set; }
46 |
47 | ///
48 | /// 如果传入的 key == UNDEFINED_KEY,则服务端返回 key
49 | ///
50 | public string key { get; private set; }
51 |
52 | ///
53 | /// Initializes a new instance of the class.
54 | ///
55 | /// Result.
56 | public QiniuUploadCompletedEventArgs(byte[] result){
57 | if(result!=null){
58 | try
59 | {
60 | string json = Encoding.UTF8.GetString(result);
61 | this.RawString = json;
62 | Dictionary dict = JsonConvert.DeserializeObject>(json);
63 | object tmp;
64 | if (dict.TryGetValue("hash", out tmp))
65 | Hash = (string)tmp;
66 | if (dict.TryGetValue("key", out tmp))
67 | key = (string)tmp;
68 | }
69 | catch (Exception e)
70 | {
71 | //throw e;
72 | }
73 | }
74 | }
75 |
76 | public QiniuUploadCompletedEventArgs(string result){
77 | this.RawString = result;
78 | Dictionary dict = JsonConvert.DeserializeObject> (result);
79 | object tmp;
80 | if (dict.TryGetValue ("hash", out tmp))
81 | Hash = (string)tmp;
82 | if (dict.TryGetValue ("key", out tmp))
83 | key = (string)tmp;
84 |
85 | }
86 | }
87 |
88 | ///
89 | /// Qiniu upload progress changed event arguments.
90 | ///
91 | public class QiniuUploadProgressChangedEventArgs:EventArgs{
92 | #region
93 | private long bytesSent;
94 | private long totalBytes;
95 | #endregion
96 |
97 | ///
98 | /// Gets the bytes sent.
99 | ///
100 | /// The bytes sent.
101 | public long BytesSent {
102 | get { return bytesSent; }
103 | }
104 |
105 | ///
106 | /// Gets the total sent.
107 | ///
108 | /// The total sent.
109 | public long TotalBytes {
110 | get { return totalBytes; }
111 | }
112 |
113 | ///
114 | /// Initializes a new instance of the class.
115 | ///
116 | /// Sent.
117 | /// Total.
118 | public QiniuUploadProgressChangedEventArgs(long sent,long total){
119 | this.bytesSent = sent;
120 | this.totalBytes = total;
121 | }
122 | }
123 |
124 | ///
125 | /// Qiniu upload block completed event arguments.
126 | ///
127 | public class QiniuUploadBlockCompletedEventArgs:EventArgs{
128 | #region vars
129 | private int index;
130 | private BlkputRet ctx;
131 | #endregion
132 |
133 | ///
134 | /// Gets the index.
135 | ///
136 | /// The index.
137 | public int Index {
138 | get { return index; }
139 | }
140 |
141 | ///
142 | /// Gets the context.
143 | ///
144 | /// The context.
145 | public BlkputRet Ctx {
146 | get { return ctx; }
147 | }
148 |
149 | ///
150 | /// Initializes a new instance of the class.
151 | ///
152 | /// Index.
153 | /// Context.
154 | public QiniuUploadBlockCompletedEventArgs(int index ,BlkputRet ctx){
155 | this.index = index;
156 | this.ctx = ctx;
157 | }
158 | }
159 |
160 | ///
161 | /// Qiniu upload block failed event arguments.
162 | ///
163 | public class QiniuUploadBlockFailedEventArgs:EventArgs{
164 |
165 | #region
166 | private int index;
167 | private Exception error;
168 | #endregion
169 |
170 | ///
171 | /// Gets the index.
172 | ///
173 | /// The index.
174 | public int Index {
175 | get { return index; }
176 | }
177 |
178 | ///
179 | /// Gets the error.
180 | ///
181 | /// The error.
182 | public Exception Error {
183 | get { return error; }
184 | }
185 |
186 | ///
187 | /// Initializes a new instance of the class.
188 | ///
189 | /// Index.
190 | /// E.
191 | public QiniuUploadBlockFailedEventArgs(int index,Exception e){
192 | this.index = index;
193 | this.error = e;
194 | }
195 | }
196 | }
197 |
198 |
--------------------------------------------------------------------------------
/sdk/QiniuPutPolicy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json;
3 | using Encoding = System.Text.UTF8Encoding;
4 |
5 | namespace qiniu
6 | {
7 | ///
8 | /// PutPolicy
9 | /// See http://developer.qiniu.com/docs/v6/api/reference/security/put-policy.html
10 | ///
11 | [JsonObject(MemberSerialization.OptIn)]
12 | public class PutPolicy
13 | {
14 | private string scope;
15 | private string callBackUrl;
16 | private string callBackBody;
17 | private string returnUrl;
18 | private string returnBody;
19 | private string saveKey;
20 | private int insertOnly;
21 | private int detectMime;
22 | private string mimeLimit;
23 | private long fsizeLimit;
24 | private string persistentOps;
25 | private string persistentNotifyUrl;
26 | private string endUser;
27 | private UInt64 expires = 3600;
28 | private UInt64 deadline = 0;
29 |
30 | ///
31 | /// 一般指文件要上传到的目标存储空间(Bucket)。若为”Bucket”,表示限定只能传到该Bucket(仅限于新增文件);若为”Bucket:Key”,表示限定特定的文件,可修改该文件。
32 | ///
33 | [JsonProperty("scope")]
34 | public string Scope {
35 | get { return scope; }
36 | set { scope = value; }
37 | }
38 |
39 | ///
40 | /// 文件上传成功后,Qiniu-Cloud-Server 向 App-Server 发送POST请求的URL,必须是公网上可以正常进行POST请求并能响应 HTTP Status 200 OK 的有效 URL
41 | ///
42 | [JsonProperty("callBackUrl")]
43 | public string CallBackUrl {
44 | get { return callBackUrl; }
45 | set { callBackUrl = value; }
46 | }
47 |
48 | ///
49 | /// 文件上传成功后,Qiniu-Cloud-Server 向 App-Server 发送POST请求的数据。支持 魔法变量 和 自定义变量,不可与 returnBody 同时使用。
50 | ///
51 | [JsonProperty("callBackBody")]
52 | public string CallBackBody {
53 | get { return callBackBody; }
54 | set { callBackBody = value; }
55 | }
56 |
57 | ///
58 | /// 设置用于浏览器端文件上传成功后,浏览器执行301跳转的URL,一般为 HTML Form 上传时使用。文件上传成功后会跳转到 returnUrl?query_string, query_string 会包含 returnBody 内容。returnUrl 不可与 callbackUrl 同时使用
59 | ///
60 | [JsonProperty("returnUrl")]
61 | public string ReturnUrl {
62 | get { return returnUrl; }
63 | set { returnUrl = value; }
64 | }
65 |
66 | ///
67 | /// 文件上传成功后,自定义从 Qiniu-Cloud-Server 最终返回給终端 App-Client 的数据。支持 魔法变量,不可与 callbackBody 同时使用。
68 | ///
69 | [JsonProperty("returnBody")]
70 | public string ReturnBody {
71 | get { return returnBody; }
72 | set { returnBody = value; }
73 | }
74 |
75 | ///
76 | /// 给上传的文件添加唯一属主标识,特殊场景下非常有用,比如根据终端用户标识给图片或视频打水印
77 | ///
78 | [JsonProperty("endUser")]
79 | public string EndUser {
80 | get { return endUser; }
81 | set { endUser = value; }
82 | }
83 |
84 | ///
85 | /// 定义 uploadToken 的失效时间,Unix时间戳,精确到秒,缺省为 3600 秒
86 | ///
87 | [JsonProperty("deadline")]
88 | public UInt64 Deadline {
89 | get { return deadline; }
90 | }
91 |
92 | ///
93 | /// 可选, Gets or sets the save key.
94 | ///
95 | /// The save key.
96 | [JsonProperty("saveKey")]
97 | public string SaveKey {
98 | get { return saveKey; }
99 | set{ saveKey = value; }
100 | }
101 |
102 | ///
103 | /// 可选。 若非0, 即使Scope为 Bucket:Key 的形式也是insert only.
104 | ///
105 | /// The insert only.
106 | [JsonProperty("insertOnly")]
107 | public int InsertOnly {
108 | get { return insertOnly; }
109 | set{ insertOnly = value; }
110 | }
111 |
112 | ///
113 | /// 可选。若非0, 则服务端根据内容自动确定 MimeType */
114 | ///
115 | /// The detect MIME.
116 | [JsonProperty("detectMime")]
117 | public int DetectMime {
118 | get {
119 | return detectMime;
120 | }
121 | set{
122 | detectMime = value;
123 | }
124 | }
125 |
126 | ///
127 | /// 限定用户上传的文件类型
128 | /// 指定本字段值,七牛服务器会侦测文件内容以判断MimeType,再用判断值跟指定值进行匹配,匹配成功则允许上传,匹配失败返回400状态码
129 | /// 示例:
130 | ///1. “image/*“表示只允许上传图片类型
131 | ///2. “image/jpeg;image/png”表示只允许上传jpg和png类型的图片
132 | ///
133 | /// The detect MIME.
134 | [JsonProperty("mimeLimit")]
135 | public string MimeLimit
136 | {
137 | get { return mimeLimit; }
138 | set { mimeLimit = value; }
139 | }
140 |
141 | ///
142 | /// 可选, Gets or sets the fsize limit.
143 | ///
144 | /// The fsize limit.
145 | [JsonProperty("fsizeLimit")]
146 | public long FsizeLimit {
147 | get { return fsizeLimit; }
148 | set{ fsizeLimit = value; }
149 | }
150 |
151 | ///
152 | /// 音视频转码持久化完成后,七牛的服务器会向用户发送处理结果通知。这里指定的url就是用于接收通知的接口。设置了`persistentOps`,则需要同时设置此字段
153 | ///
154 | [JsonProperty("persistentNotifyUrl")]
155 | public string PersistentNotifyUrl {
156 | get { return persistentNotifyUrl; }
157 | set { persistentNotifyUrl = value; }
158 | }
159 |
160 | ///
161 | /// 可指定音视频文件上传完成后,需要进行的转码持久化操作。asyncOps的处理结果保存在缓存当中,有可能失效。而persistentOps的处理结果以文件形式保存在bucket中,体验更佳。[数据处理(持久化)](http://docs.qiniu.com/api/persistent-ops.html
162 | ///
163 | [JsonProperty("persistentOps")]
164 | public string PersistentOps {
165 | get { return persistentOps; }
166 | set { persistentOps = value; }
167 | }
168 |
169 | ///
170 | /// Initializes a new instance of the class.
171 | ///
172 | /// Scope.
173 | /// Expires.
174 | public PutPolicy (string scope, UInt32 expires=3600)
175 | {
176 | this.scope = scope;
177 | this.expires = expires;
178 | }
179 |
180 | ///
181 | /// 生成上传Token
182 | ///
183 | ///
184 | public string Token (MAC mac=null)
185 | {
186 | if (string.IsNullOrEmpty (persistentOps) ^ string.IsNullOrEmpty (persistentNotifyUrl)) {
187 | throw new Exception ("PersistentOps and PersistentNotifyUrl error");
188 | }
189 | if (string.IsNullOrEmpty (callBackUrl) ^ string.IsNullOrEmpty (callBackBody)) {
190 | throw new Exception ("CallBackUrl and CallBackBody error");
191 | }
192 | if (!string.IsNullOrEmpty (returnUrl) && !string.IsNullOrEmpty (callBackUrl)) {
193 | throw new Exception ("returnUrl and callBackUrl error");
194 | }
195 | if (mac == null) {
196 | mac = new MAC (Config.ACCESS_KEY, Config.SECRET_KEY);
197 | }
198 | this.deadline = (UInt32)((DateTime.Now.ToUniversalTime ().Ticks - 621355968000000000) / 10000000 + (long)expires);
199 | string flag = this.ToString ();
200 | return mac.SignWithData (Encoding.UTF8.GetBytes (flag));
201 | }
202 |
203 | ///
204 | /// Returns a that represents the current in json formmat.
205 | ///
206 | /// A that represents the current .
207 | public override string ToString ()
208 | {
209 | return JsonConvert.SerializeObject (this);
210 | }
211 | }
212 | }
213 |
214 |
215 |
--------------------------------------------------------------------------------
/sdk/QiniuWebClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Timers;
3 | using System.Threading;
4 | using System.IO;
5 | using System.Collections.Specialized;
6 | using System.Net;
7 |
8 | namespace qiniu
9 | {
10 | public class QiniuWebClient:WebClient
11 | {
12 | public event EventHandler Timeout;
13 | ///
14 | /// Ons the timeout.
15 | ///
16 | protected virtual void onTimeout(){
17 | if (this.Timeout != null) {
18 | this.Timeout (this,new EventArgs());
19 | }
20 | }
21 |
22 | private bool isUploading = true;
23 | private object isUploadinglocker = new object ();
24 |
25 | ManualResetEvent done = new ManualResetEvent (false);
26 |
27 | System.Timers.Timer timer;
28 |
29 | private string uptoken;
30 |
31 | ///
32 | /// Gets or sets upload token.
33 | ///
34 | /// Up token.
35 | public string UpToken {
36 | get { return uptoken; }
37 | set {
38 | uptoken = value;
39 | this.Headers.Add ("Authorization", "UpToken " + this.uptoken);
40 | }
41 | }
42 |
43 | ///
44 | /// Initializes a new instance of the class.
45 | ///
46 | /// Timeout.
47 | public QiniuWebClient(double timeout = 100000.0){
48 |
49 | this.timer = new System.Timers.Timer (timeout);
50 | this.timer.Elapsed+= (object sender, ElapsedEventArgs e) => {
51 | if(!isUploading){
52 | onTimeout();
53 | done.Set();
54 | return;
55 | }
56 | lock(isUploadinglocker){
57 | isUploading = false;
58 | }
59 | };
60 | }
61 |
62 | ///
63 | /// Posts the form.
64 | ///
65 | /// URL.
66 | /// Values.
67 | /// Filename.
68 | public void AsyncPostForm(string url,NameValueCollection formData,string filename){
69 | FileInfo fileInfo = new FileInfo (filename);
70 | string boundary = RandomBoundary ();
71 | using (FileStream fileStream = fileInfo.OpenRead())
72 | using (Stream postDataStream = GetPostStream (fileStream, formData ["key"], formData, boundary)) {
73 | this.Headers.Add ("Content-Type", "multipart/form-data; boundary=" + boundary);
74 | byte[] hugeBuffer = new byte[postDataStream.Length];
75 | postDataStream.Seek (0, SeekOrigin.Begin);
76 | postDataStream.Read (hugeBuffer, 0, (int)postDataStream.Length);
77 | UploadDataAsync (new Uri(url), "POST", hugeBuffer);
78 | }
79 | }
80 |
81 | ///
82 | /// Posts the form.
83 | ///
84 | /// URL.
85 | /// Values.
86 | /// Filename.
87 | public byte[] PostForm(string url,NameValueCollection formData,string filename){
88 | FileInfo fileInfo = new FileInfo (filename);
89 | string boundary = RandomBoundary ();
90 | using (FileStream fileStream = fileInfo.OpenRead())
91 | using (Stream postDataStream = GetPostStream (fileStream, formData ["key"], formData, boundary)) {
92 | this.Headers.Add ("Content-Type", "multipart/form-data; boundary=" + boundary);
93 | byte[] hugeBuffer = new byte[postDataStream.Length];
94 | postDataStream.Seek (0, SeekOrigin.Begin);
95 | postDataStream.Read (hugeBuffer, 0, (int)postDataStream.Length);
96 | return UploadData (new Uri(url), "POST", hugeBuffer);
97 | }
98 | }
99 |
100 | ///
101 | /// Call the specified url and mac.
102 | ///
103 | /// URL.
104 | /// Mac.
105 | public string Call(string url,MAC mac){
106 | try{
107 | HttpWebRequest req = this.GetWebRequest (new Uri (url)) as HttpWebRequest;
108 | mac.SignRequest (req, null);
109 | using (HttpWebResponse response = req.GetResponse() as HttpWebResponse) {
110 | using (StreamReader reader = new StreamReader(response.GetResponseStream())) {
111 | return reader.ReadToEnd ();
112 | }
113 | }
114 | }catch(Exception e){
115 | throw e;
116 | }
117 | }
118 |
119 | protected override void OnUploadDataCompleted (UploadDataCompletedEventArgs e)
120 | {
121 | base.OnUploadDataCompleted (e);
122 | done.Set ();
123 | }
124 |
125 | protected override void OnUploadProgressChanged (UploadProgressChangedEventArgs e)
126 | {
127 | lock (isUploadinglocker) {
128 | isUploading = true;
129 | }
130 | base.OnUploadProgressChanged (e);
131 | }
132 |
133 | ///
134 | /// Is the upload data async.
135 | ///
136 | /// The upload data async.
137 | /// URL.
138 | /// Data.
139 | public void iUploadDataAsync(string url,string method,byte[] data){
140 |
141 | this.UploadDataAsync (new Uri (url),method, data);
142 | this.timer.Start ();
143 | done.WaitOne ();
144 | return;
145 | }
146 |
147 |
148 | private Stream GetPostStream (Stream putStream, string fileName, NameValueCollection formData, string boundary)
149 | {
150 | Stream postDataStream = new System.IO.MemoryStream ();
151 |
152 | //adding form data
153 |
154 | string formDataHeaderTemplate = Environment.NewLine + "--" + boundary + Environment.NewLine +
155 | "Content-Disposition: form-data; name=\"{0}\";" + Environment.NewLine + Environment.NewLine + "{1}";
156 |
157 | foreach (string key in formData.Keys) {
158 | byte[] formItemBytes = System.Text.Encoding.UTF8.GetBytes (string.Format (formDataHeaderTemplate,
159 | key, formData [key]));
160 | postDataStream.Write (formItemBytes, 0, formItemBytes.Length);
161 | }
162 |
163 | //adding file,Stream data
164 | #region adding file data
165 |
166 | string fileHeaderTemplate = Environment.NewLine + "--" + boundary + Environment.NewLine +
167 | "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"" +
168 | Environment.NewLine + "Content-Type: application/octet-stream" + Environment.NewLine + Environment.NewLine;
169 | byte[] fileHeaderBytes = System.Text.Encoding.UTF8.GetBytes (string.Format (fileHeaderTemplate,
170 | "file", fileName));
171 | postDataStream.Write (fileHeaderBytes, 0, fileHeaderBytes.Length);
172 |
173 | byte[] buffer = new byte[1024*1024];
174 | int bytesRead = 0;
175 | while ((bytesRead = putStream.Read(buffer, 0, buffer.Length)) != 0) {
176 | postDataStream.Write (buffer, 0, bytesRead);
177 | }
178 | putStream.Close ();
179 | #endregion
180 |
181 | #region adding end
182 | byte[] endBoundaryBytes = System.Text.Encoding.UTF8.GetBytes (Environment.NewLine + "--" + boundary + "--" + Environment.NewLine);
183 | postDataStream.Write (endBoundaryBytes, 0, endBoundaryBytes.Length);
184 | #endregion
185 |
186 | return postDataStream;
187 |
188 | }
189 |
190 | private string RandomBoundary (){
191 | return String.Format ("----------{0:N}", Guid.NewGuid ());
192 | }
193 |
194 | ///
195 | ///
196 | ///
197 | ///
198 | protected override void Dispose(bool disposing)
199 | {
200 | if (timer.Enabled) {
201 | timer.Dispose();
202 | }
203 | base.Dispose(disposing);
204 | }
205 | }
206 | }
207 |
208 |
--------------------------------------------------------------------------------
/demo/Base64File.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace demo
5 | {
6 | public class Base64File
7 | {
8 | int fileSize = -1;
9 | string base64Content;
10 |
11 | public int FileSize {
12 | get {
13 | if (this.fileSize < 0) {
14 | this.fileSize=Convert.FromBase64String (this.base64Content).Length;
15 | }
16 | return this.fileSize;
17 | }
18 | }
19 |
20 | public Base64File (string base64Content)
21 | {
22 | this.base64Content = base64Content;
23 | }
24 |
25 | public void Save(string filename){
26 | byte[] fileContent = Convert.FromBase64String(this.base64Content);
27 | File.WriteAllBytes (filename, fileContent);
28 | }
29 |
30 | public static string TESTBASE64 = "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";
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/sdk/QiniuFile.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using System.IO;
6 | using System.Net;
7 | using System.Collections.Specialized;
8 | using Newtonsoft.Json;
9 | using Encoding = System.Text.UTF8Encoding;
10 |
11 | namespace qiniu
12 | {
13 | public class QiniuFile
14 | {
15 |
16 | #region events
17 | ///
18 | /// Occurs when upload progress changed.
19 | ///
20 | public event EventHandler UploadProgressChanged;
21 | protected void onQiniuUploadProgressChanged(QiniuUploadProgressChangedEventArgs e){
22 | if (this.UploadProgressChanged != null) {
23 | UploadProgressChanged (this,e);
24 | }
25 | }
26 |
27 | ///
28 | /// Occurs when upload failed.
29 | ///
30 | public event EventHandler UploadFailed;
31 | protected void onUploadFailed(QiniuUploadFailedEventArgs e){
32 | if (this.UploadFailed != null) {
33 | UploadFailed (this,e);
34 | }
35 | }
36 |
37 | ///
38 | /// Occurs when upload completed.
39 | ///
40 | public event EventHandler UploadCompleted;
41 | protected void onQiniuUploadCompleted(QiniuUploadCompletedEventArgs e){
42 | if (this.UploadCompleted != null) {
43 | UploadCompleted (this,e);
44 | }
45 | }
46 |
47 | ///
48 | /// Occurs when upload block completed.
49 | ///
50 | public event EventHandler UploadBlockCompleted;
51 | protected void onQiniuUploadBlockCompleted(QiniuUploadBlockCompletedEventArgs e){
52 | if (this.UploadBlockCompleted != null) {
53 | UploadBlockCompleted (this,e);
54 | }
55 | }
56 |
57 | ///
58 | /// Occurs when upload block failed.
59 | ///
60 | public event EventHandler UploadBlockFailed;
61 | protected void onQiniuUploadBlockFailed(QiniuUploadBlockFailedEventArgs e){
62 | if (this.UploadBlockFailed != null) {
63 | UploadBlockFailed (this,e);
64 | }
65 | }
66 |
67 | ///
68 | /// Occurs when upload canceled.
69 | ///
70 | public event EventHandler UploadCanceled;
71 | protected void onQiniuUploadCanceld(){
72 | UploadCanceled (this, null);
73 | }
74 | #endregion
75 |
76 | #region localVars
77 | private string bucketName;
78 | private string key;
79 | private string localfile;
80 | private bool uploading = false;
81 | #endregion
82 |
83 | #region construct
84 | ///
85 | /// Initializes a new instance of the class.
86 | ///
87 | public QiniuFile (string bucketName)
88 | {
89 | this.bucketName = bucketName;
90 | }
91 |
92 | ///
93 | /// Initializes a new instance of the class.
94 | ///
95 | /// Bucket.
96 | /// Key.
97 | public QiniuFile(string bucketName,string key){
98 | this.bucketName = bucketName;
99 | this.key = key;
100 | }
101 |
102 | ///
103 | /// Initializes a new instance of the class.
104 | ///
105 | /// Bucket.
106 | /// Key.
107 | /// Localfile.
108 | public QiniuFile(string bucketName,string key,string localfile){
109 | this.bucketName = bucketName;
110 | this.key = key;
111 | if (!File.Exists(localfile))
112 | {
113 | throw new Exception(string.Format("{0} does not exist", localfile));
114 | }
115 | this.localfile = localfile;
116 | }
117 | #endregion
118 |
119 | ///
120 | /// Stat the QiniuFile with mac.
121 | ///
122 | /// Mac.
123 | public QiniuFileInfo Stat (MAC mac=null){
124 | if (mac == null)
125 | mac = new MAC ();
126 | string url = string.Format ("{0}/{1}/{2}", Config.RS_HOST, "stat", Base64URLSafe.Encode (this.bucketName + ":" + this.key));
127 | try {
128 | using (QiniuWebClient client = new QiniuWebClient ()) {
129 | string result = client.Call (url, mac);
130 | return GetQiniuEntry (result);
131 | }
132 | } catch (WebException e) {
133 | throw new QiniuWebException(e);
134 | }catch(Exception e){
135 | throw e;
136 | }
137 | }
138 |
139 | ///
140 | /// Delete the QiniuFile with mac.
141 | ///
142 | /// Mac.
143 | public bool Delete(MAC mac=null){
144 |
145 | if (mac == null)
146 | mac = new MAC ();
147 | string url = string.Format ("{0}/{1}/{2}", Config.RS_HOST, "delete", Base64URLSafe.Encode (this.bucketName + ":" + this.key));
148 | try {
149 | using (QiniuWebClient client = new QiniuWebClient ()) {
150 | client.Call (url, mac);
151 | return true;
152 | }
153 | } catch (WebException e) {
154 | throw new QiniuWebException(e);
155 | } catch (Exception e) {
156 | throw e;
157 | }
158 | }
159 |
160 | ///
161 | /// Move the specified destBucket, destKey and mac.
162 | ///
163 | /// Destination bucket.
164 | /// Destination key.
165 | /// Mac.
166 | public bool Move(string destBucket,string destKey,MAC mac=null){
167 | if (mac == null) {
168 | mac = new MAC ();
169 | }
170 | string url = string.Format ("{0}/{1}/{2}/{3}",
171 | Config.RS_HOST,
172 | "move",
173 | Base64URLSafe.Encode (this.bucketName+":"+this.key),
174 | Base64URLSafe.Encode (destBucket+":"+destKey));
175 | try {
176 | using (QiniuWebClient client = new QiniuWebClient ()) {
177 | client.Call (url, mac);
178 | return true;
179 | }
180 | } catch (WebException e) {
181 | throw new QiniuWebException(e);
182 | } catch (Exception e) {
183 | throw e;
184 | }
185 | }
186 |
187 | ///
188 | /// Uploads the string.
189 | ///
190 | /// Base64 content.
191 | public void UploadString(int filesize,string mimeType,string base64Content){
192 | string token = getDefalutToken (this.bucketName, this.key);
193 | UploadString (token, filesize, mimeType, base64Content);
194 | }
195 |
196 | ///
197 | /// Uploads the string.
198 | ///
199 | /// Token.
200 | /// Base64 content.
201 | public void UploadString(string token,int fileSize,string mimeType,string base64Content){
202 | using (QiniuWebClient qwc = new QiniuWebClient ()) {
203 | qwc.UpToken = token;
204 | string url = Config.UP_HOST +
205 | string.Format("/putb64/{0}/key/{1}/mimeType/{2}",
206 | fileSize,
207 | Base64URLSafe.Encode(this.key),
208 | Base64URLSafe.Encode(mimeType));
209 |
210 | qwc.UploadStringCompleted += (sender, e) => {
211 | if (e.Error != null && e.Error is WebException) {
212 | if (e.Error is WebException) {
213 | QiniuWebException qwe = new QiniuWebException (e.Error as WebException);
214 | onUploadFailed (new QiniuUploadFailedEventArgs (qwe));
215 | } else {
216 | onUploadFailed (new QiniuUploadFailedEventArgs (e.Error));
217 | }
218 | } else {
219 | onQiniuUploadCompleted(new QiniuUploadCompletedEventArgs(e.Result));
220 |
221 | onQiniuUploadCompleted (new QiniuUploadCompletedEventArgs (e.Result));
222 | }
223 | };
224 |
225 | qwc.UploadProgressChanged += (sender, e) => {
226 | onQiniuUploadProgressChanged (new QiniuUploadProgressChangedEventArgs (e.BytesSent, e.TotalBytesToSend));
227 | };
228 |
229 | qwc.Headers.Add("Content-Type", "application/octet-stream");
230 | qwc.UploadStringAsync (new Uri (url), "POST", base64Content);
231 | }
232 | }
233 |
234 | ///
235 | /// Asyncs the upload.
236 | ///
237 | public void Upload(BlkputRet[] blkRets=null){
238 | if (!uploading) {
239 | string token = getDefalutToken (this.bucketName,this.key);
240 | Upload (token,blkRets);
241 | }
242 | }
243 |
244 | ///
245 | /// Upload the specified token.
246 | ///
247 | /// Token.
248 | public void Upload(string token,BlkputRet[] blkRets=null){
249 | if (uploading) {
250 | return;
251 | }
252 |
253 | FileInfo finfo = new FileInfo (this.localfile);
254 | if (finfo.Length < (long)(1024 * 1024 * 8)) {
255 | uploadSmallFile (token);
256 | } else {
257 | uploadBigFile (token,blkRets);
258 | }
259 | }
260 |
261 | ///
262 | /// Uploads the small file ( < 8MB).
263 | ///
264 | /// Token.
265 | private void uploadSmallFile(string token){
266 | if (uploading) {
267 | return;
268 | }
269 | uploading = true;
270 | NameValueCollection formData = new NameValueCollection ();
271 | formData ["key"] = this.key;
272 | formData ["token"] = token;
273 | try {
274 | using (QiniuWebClient qwc = new QiniuWebClient ()) {
275 | qwc.UploadDataCompleted += (sender, e) => {
276 | if (e.Error != null && e.Error is WebException) {
277 | if (e.Error is WebException) {
278 | QiniuWebException qwe = new QiniuWebException (e.Error as WebException);
279 | onUploadFailed (new QiniuUploadFailedEventArgs (qwe));
280 | } else {
281 | onUploadFailed (new QiniuUploadFailedEventArgs (e.Error));
282 | }
283 | } else {
284 | onQiniuUploadCompleted (new QiniuUploadCompletedEventArgs (e.Result));
285 | }
286 | };
287 | qwc.UploadProgressChanged += (sender, e) => {
288 | onQiniuUploadProgressChanged (new QiniuUploadProgressChangedEventArgs (e.BytesSent, e.TotalBytesToSend));
289 | };
290 | qwc.AsyncPostForm (Config.UP_HOST, formData, this.localfile);
291 | }
292 | } catch (WebException we) {
293 | onUploadFailed (new QiniuUploadFailedEventArgs (new QiniuWebException (we)));
294 | } catch (Exception e) {
295 | onUploadFailed (new QiniuUploadFailedEventArgs (e));
296 | } finally {
297 | uploading = false;
298 | }
299 | }
300 |
301 | ///
302 | /// Uploads the big file ( > 8MB ).
303 | ///
304 | /// Token.
305 | private void uploadBigFile(string token,BlkputRet[] puttedBlk=null){
306 | uploading = true;
307 | Action a = () => {
308 | FileInfo finfo = new FileInfo (this.localfile);
309 | int blockcount = block_count (finfo.Length);
310 |
311 | BlkputRet []blkRets = new BlkputRet[blockcount];
312 | using (FileStream fs = File.OpenRead (this.localfile)) {
313 | long totalSent = 0;
314 | int readLen = BLOCKSIZE;
315 | byte[] buf = new byte[readLen];
316 | for (int i = 0; i < blockcount; i++) {
317 | if (puttedBlk!=null&&i< puttedBlk.Length) {
318 | blkRets[i] = puttedBlk[i];
319 | totalSent +=(long)blkRets[i].offset;
320 | continue;
321 | }
322 | if (i == blockcount - 1) {
323 | readLen = (int)(finfo.Length - i * BLOCKSIZE);
324 | buf = new byte[readLen];
325 | }
326 | fs.Seek ((long)i * BLOCKSIZE, SeekOrigin.Begin);
327 | fs.Read (buf, 0, readLen);
328 | using (QiniuWebClient client = new QiniuWebClient ()) {
329 | bool failed = false;
330 | client.UploadDataCompleted += (sender, e) => {
331 | if (e.Error != null) {
332 | onQiniuUploadBlockFailed (new QiniuUploadBlockFailedEventArgs (i, e.Error));
333 | failed =true;
334 | return;
335 | } else {
336 | blkRets [i] = GetBlkPutRet (e.Result);
337 | onQiniuUploadBlockCompleted (new QiniuUploadBlockCompletedEventArgs (i, blkRets [i]));
338 | }
339 | };
340 | client.UploadProgressChanged += (sender, e) => {
341 | onQiniuUploadProgressChanged (new QiniuUploadProgressChangedEventArgs (totalSent + e.BytesSent, finfo.Length));
342 | };
343 | client.Timeout+= (sender, e) => {
344 | onQiniuUploadBlockFailed(new QiniuUploadBlockFailedEventArgs(i,new Exception("QiniuWebClient Timeout.")));
345 | failed = true;
346 | };
347 | client.UpToken = token;
348 | client.Headers.Add("Content-Type", "application/octet-stream");
349 | string url = string.Format("{0}/mkblk/{1}", Config.UP_HOST, readLen);
350 | client.iUploadDataAsync ( url, "POST", buf);
351 | if(failed){
352 | return;
353 | }
354 | totalSent += readLen;
355 | }
356 | }
357 | }
358 | try {
359 | byte[] result = Mkfile (blkRets, token, this.key, finfo.Length);
360 | if (result != null && result.Length > 0) {
361 | onQiniuUploadCompleted (new QiniuUploadCompletedEventArgs (result));
362 | }
363 | } catch (Exception e) {
364 | onUploadFailed (new QiniuUploadFailedEventArgs (e));
365 | }
366 | };
367 | a.BeginInvoke (null, null);
368 | }
369 |
370 | ///
371 | /// Mkfile the specified blkRets, key and fsize.
372 | ///
373 | /// Blk rets.
374 | /// Key.
375 | /// Fsize.
376 | private static byte[] Mkfile(BlkputRet[] blkRets,string token, string key, long fsize)
377 | {
378 | StringBuilder urlBuilder = new StringBuilder();
379 | urlBuilder.AppendFormat("{0}/mkfile/{1}", Config.UP_HOST, fsize);
380 | if (!string.IsNullOrEmpty(key))
381 | {
382 | urlBuilder.AppendFormat("/key/{0}", Base64URLSafe.ToBase64URLSafe(key));
383 | }
384 | int proCount = blkRets.Length;
385 | using (Stream body = new MemoryStream())
386 | {
387 | for (int i = 0; i < proCount; i++)
388 | {
389 | byte[] bctx = Encoding.ASCII.GetBytes(blkRets[i].ctx);
390 | body.Write(bctx, 0, bctx.Length);
391 | if (i != proCount - 1)
392 | {
393 | body.WriteByte((byte)',');
394 | }
395 | }
396 | body.Seek(0, SeekOrigin.Begin);
397 | byte[] data = new byte[body.Length];
398 | body.Read (data, 0, (int)body.Length);
399 | using(QiniuWebClient client = new QiniuWebClient ()){
400 | client.UpToken = token;
401 | client.Headers.Add("Content-Type", "application/octet-stream");
402 | return client.UploadData (urlBuilder.ToString(), "POST", data);
403 | }
404 | }
405 | }
406 |
407 | #region Config
408 | private const int blockBits = 22;
409 | private readonly static int BLOCKSIZE = 1 << blockBits;
410 | private readonly static int blockMask = BLOCKSIZE - 1;
411 | private static int block_count(long fsize)
412 | {
413 | return (int)((fsize + blockMask) >> blockBits);
414 | }
415 | private static BlkputRet GetBlkPutRet(byte[] result){
416 | return JsonConvert.DeserializeObject (Encoding.UTF8.GetString (result));
417 | }
418 | private static QiniuFileInfo GetQiniuEntry(string result){
419 | return JsonConvert.DeserializeObject (result);
420 | }
421 | private static string getDefalutToken(string bucketName,string key){
422 | string scope = bucketName;
423 | if(!string.IsNullOrEmpty(key)){
424 | scope += ":" + key;
425 | }
426 | PutPolicy policy = new PutPolicy (scope, 3600 * 24);
427 | return policy.Token ();
428 | }
429 | #endregion
430 | }
431 |
432 | [JsonObject(MemberSerialization.OptIn)]
433 | public class BlkputRet
434 | {
435 | [JsonProperty("ctx")]
436 | public string ctx;
437 | [JsonProperty("checksum")]
438 | public string checkSum;
439 | [JsonProperty("crc32")]
440 | public UInt32 crc32;
441 | [JsonProperty("offset")]
442 | public ulong offset;
443 | }
444 |
445 | [JsonObject(MemberSerialization.OptIn)]
446 | public class QiniuFileInfo
447 | {
448 | ///
449 | /// 文件的Hash值
450 | ///
451 | /// true if this instance hash; otherwise, false.
452 | [JsonProperty("hash")]
453 | public string Hash;
454 |
455 | ///
456 | /// 文件的大小(单位: 字节)
457 | ///
458 | /// The fsize.
459 | [JsonProperty("fsize")]
460 | public long Fsize;
461 |
462 | ///
463 | /// 文件上传到七牛云的时间(Unix时间戳)
464 | ///
465 | /// The put time.
466 | [JsonProperty("putTime")]
467 | public long PutTime;
468 |
469 | ///
470 | /// 文件的媒体类型,比如"image/gif"
471 | ///
472 | /// The type of the MIME.
473 | [JsonProperty("mimeType")]
474 | public string MimeType;
475 |
476 | ///
477 | /// Gets the customer.
478 | ///
479 | /// The customer.
480 | [JsonProperty("customer")]
481 | public string Customer;
482 |
483 | }
484 | }
485 |
486 |
--------------------------------------------------------------------------------