├── .gitignore
├── LICENSE
├── LanZouCloud.sln
├── README.md
├── src
└── LanZouCloud
│ ├── Code
│ ├── LanZouCloud.Http.cs
│ ├── LanZouCloud.Log.cs
│ ├── LanZouCloud.Utils.cs
│ ├── LanZouCloud.cs
│ ├── LanZouCode.cs
│ └── LanZouModels.cs
│ └── LanZouCloud.csproj
└── tests
└── LanZouCloud.Tests
├── AConfig.cs
├── CommonTest.cs
├── DownloadTest.cs
├── LanZouCloud.Tests.csproj
└── UploadTest.cs
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # NuGet Symbol Packages
188 | *.snupkg
189 | # The packages folder can be ignored because of Package Restore
190 | **/[Pp]ackages/*
191 | # except build/, which is used as an MSBuild target.
192 | !**/[Pp]ackages/build/
193 | # Uncomment if necessary however generally it will be regenerated when needed
194 | #!**/[Pp]ackages/repositories.config
195 | # NuGet v3's project.json files produces more ignorable files
196 | *.nuget.props
197 | *.nuget.targets
198 |
199 | # Microsoft Azure Build Output
200 | csx/
201 | *.build.csdef
202 |
203 | # Microsoft Azure Emulator
204 | ecf/
205 | rcf/
206 |
207 | # Windows Store app package directories and files
208 | AppPackages/
209 | BundleArtifacts/
210 | Package.StoreAssociation.xml
211 | _pkginfo.txt
212 | *.appx
213 | *.appxbundle
214 | *.appxupload
215 |
216 | # Visual Studio cache files
217 | # files ending in .cache can be ignored
218 | *.[Cc]ache
219 | # but keep track of directories ending in .cache
220 | !?*.[Cc]ache/
221 |
222 | # Others
223 | ClientBin/
224 | ~$*
225 | *~
226 | *.dbmdl
227 | *.dbproj.schemaview
228 | *.jfm
229 | *.pfx
230 | *.publishsettings
231 | orleans.codegen.cs
232 |
233 | # Including strong name files can present a security risk
234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
235 | #*.snk
236 |
237 | # Since there are multiple workflows, uncomment next line to ignore bower_components
238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
239 | #bower_components/
240 |
241 | # RIA/Silverlight projects
242 | Generated_Code/
243 |
244 | # Backup & report files from converting an old project file
245 | # to a newer Visual Studio version. Backup files are not needed,
246 | # because we have git ;-)
247 | _UpgradeReport_Files/
248 | Backup*/
249 | UpgradeLog*.XML
250 | UpgradeLog*.htm
251 | ServiceFabricBackup/
252 | *.rptproj.bak
253 |
254 | # SQL Server files
255 | *.mdf
256 | *.ldf
257 | *.ndf
258 |
259 | # Business Intelligence projects
260 | *.rdl.data
261 | *.bim.layout
262 | *.bim_*.settings
263 | *.rptproj.rsuser
264 | *- [Bb]ackup.rdl
265 | *- [Bb]ackup ([0-9]).rdl
266 | *- [Bb]ackup ([0-9][0-9]).rdl
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # CodeRush personal settings
303 | .cr/personal
304 |
305 | # Python Tools for Visual Studio (PTVS)
306 | __pycache__/
307 | *.pyc
308 |
309 | # Cake - Uncomment if you are using it
310 | # tools/**
311 | # !tools/packages.config
312 |
313 | # Tabs Studio
314 | *.tss
315 |
316 | # Telerik's JustMock configuration file
317 | *.jmconfig
318 |
319 | # BizTalk build output
320 | *.btp.cs
321 | *.btm.cs
322 | *.odx.cs
323 | *.xsd.cs
324 |
325 | # OpenCover UI analysis results
326 | OpenCover/
327 |
328 | # Azure Stream Analytics local run output
329 | ASALocalRun/
330 |
331 | # MSBuild Binary and Structured Log
332 | *.binlog
333 |
334 | # NVidia Nsight GPU debugger configuration file
335 | *.nvuser
336 |
337 | # MFractors (Xamarin productivity tool) working folder
338 | .mfractor/
339 |
340 | # Local History for Visual Studio
341 | .localhistory/
342 |
343 | # BeatPulse healthcheck temp database
344 | healthchecksdb
345 |
346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
347 | MigrationBackup/
348 |
349 | # Ionide (cross platform F# VS Code tools) working folder
350 | .ionide/
351 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 psygames
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.
22 |
--------------------------------------------------------------------------------
/LanZouCloud.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.31515.178
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LanZouCloud", "src\LanZouCloud\LanZouCloud.csproj", "{CE5465CB-FDBC-44A6-A3A4-90893A84E1F5}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LanZouCloud.Tests", "tests\LanZouCloud.Tests\LanZouCloud.Tests.csproj", "{36E405EA-FCD7-4878-9EB9-E51785F742CE}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {CE5465CB-FDBC-44A6-A3A4-90893A84E1F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {CE5465CB-FDBC-44A6-A3A4-90893A84E1F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {CE5465CB-FDBC-44A6-A3A4-90893A84E1F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {CE5465CB-FDBC-44A6-A3A4-90893A84E1F5}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {36E405EA-FCD7-4878-9EB9-E51785F742CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {36E405EA-FCD7-4878-9EB9-E51785F742CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {36E405EA-FCD7-4878-9EB9-E51785F742CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {36E405EA-FCD7-4878-9EB9-E51785F742CE}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {A4EC49FE-60A6-48D5-84A5-8F360F70FBCD}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | - 蓝奏云API -
6 |
7 | ### 好消息,好消息!!! [code](https://github.com/psygames/LanZouCloud-API-Sharp/tree/code) 分支仅包含代码部分,可以作为子树合并到自己的项目中。
8 | ### 技术交流QQ群 >>>[点击加入](https://jq.qq.com/?_wv=1027&k=i87alUFD)<<<
9 |
10 | # 简介
11 |
12 | - 本库封装了蓝奏网盘的基础功能:
13 | - [x] 登录
14 | - [x] 注销
15 | - [x] 获取文件(夹)列表
16 | - [x] 下载文件
17 | - [x] 上传文件
18 | - [x] 删除文件(夹)
19 | - [x] 移动文件
20 | - [x] 创建文件夹
21 | - [x] 设置文件(夹)访问密码
22 | - [x] 设置文件(夹)描述
23 | - [ ] 清空回收站
24 | - [ ] 从回收站恢复文件(夹)
25 |
26 | - 同时增加了以下功能:
27 | - [x] 获取下载直链
28 | - [x] 下载时断点续传
29 | - [x] 移动文件夹
30 | - [ ] 上传文件夹
31 | - [ ] 下载文件夹
32 | - [ ] 清理"幽灵"文件夹
33 |
34 | - 如果有任何问题或建议, 欢迎提 issue, 维护不易,求一个 star (\*/ω\*)
35 | - python 版请关注:[https://github.com/zaxtyson/LanZouCloud-API](https://github.com/zaxtyson/LanZouCloud-API)
36 |
37 | # 免责声明
38 |
39 | - 本项目仅供个人学习使用,严禁用于商业用途
40 | - **本项目没有任何担保**,如果您使用这些代码,您必需承担其带来的风险
41 |
--------------------------------------------------------------------------------
/src/LanZouCloud/Code/LanZouCloud.Http.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Net;
5 | using System.Net.Http;
6 | using System.Net.Http.Headers;
7 | using System.Text;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace LanZouCloudAPI
12 | {
13 | public partial class LanZouCloud
14 | {
15 | private const int http_retries = 3;
16 | private CookieContainer cookieContainer = new CookieContainer();
17 | private void _set_cookie(string domain, string name, string value)
18 | {
19 | cookieContainer.Add(new Cookie(name, value, null, domain));
20 | }
21 |
22 | // 需要自己处理超时重试 !!!
23 | private HttpClient _get_client(Dictionary headers = null,
24 | float timeout = 0, bool allowRedirect = true, string proxy = null)
25 | {
26 | var handler = new HttpClientHandler();
27 | handler.UseCookies = true;
28 | handler.AllowAutoRedirect = allowRedirect;
29 | handler.CookieContainer = cookieContainer;
30 |
31 | var client = new HttpClient(handler, true);
32 |
33 | headers = headers ?? _headers;
34 |
35 | if (headers != null)
36 | {
37 | foreach (var item in headers)
38 | {
39 | client.DefaultRequestHeaders.Add(item.Key, item.Value);
40 | }
41 | }
42 |
43 | timeout = timeout > 0 ? timeout : _timeout;
44 | client.Timeout = new TimeSpan((long)(timeout * 10000000L));
45 |
46 | proxy = proxy ?? _proxy;
47 | if (proxy != null)
48 | {
49 | handler.UseProxy = true;
50 | handler.Proxy = new WebProxy(proxy);
51 | }
52 |
53 | return client;
54 | }
55 |
56 | #region 网络超时,自动重试
57 | private async Task _get_text(string url)
58 | {
59 | url = fix_url_domain(url);
60 | string text = null;
61 | for (int i = 0; i < http_retries; i++)
62 | {
63 | try
64 | {
65 | using (var client = _get_client())
66 | {
67 | using (var resp = await client.GetAsync(url))
68 | {
69 | resp.EnsureSuccessStatusCode();
70 | text = await resp.Content.ReadAsStringAsync();
71 | }
72 | }
73 | break;
74 | }
75 | catch (Exception ex)
76 | {
77 | Log($"Http Error: {ex.Message}", LogLevel.Error, nameof(_get_text));
78 | if (i < http_retries) Log($"Retry({i + 1}): {url}", LogLevel.Info, nameof(_get_text));
79 | }
80 | }
81 | return text;
82 | }
83 |
84 | private async Task _post_text(string url, Dictionary data)
85 | {
86 | url = fix_url_domain(url);
87 | string text = null;
88 | for (int i = 0; i < http_retries; i++)
89 | {
90 | try
91 | {
92 | using (var client = _get_client())
93 | {
94 | using (var content = new FormUrlEncodedContent(data))
95 | {
96 | using (var resp = await client.PostAsync(url, content))
97 | {
98 | resp.EnsureSuccessStatusCode();
99 | text = await resp.Content.ReadAsStringAsync();
100 | }
101 | }
102 | }
103 | break;
104 | }
105 | catch (Exception ex)
106 | {
107 | Log($"Http Error: {ex.Message}", LogLevel.Error, nameof(_post_text));
108 | if (i < http_retries) Log($"Retry({i + 1}): {url}", LogLevel.Info, nameof(_post_text));
109 | }
110 | }
111 | return text;
112 | }
113 |
114 | private async Task _get_content_length(string url)
115 | {
116 | url = fix_url_domain(url);
117 | long? content_length = null;
118 | for (int i = 0; i < http_retries; i++)
119 | {
120 | try
121 | {
122 | using (var client = _get_client(null, 0, false))
123 | {
124 | using (var resp = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead))
125 | {
126 | if (resp.StatusCode == HttpStatusCode.InternalServerError)
127 | break;
128 | // dont ensure success code, casue code is 502 Bad Gateway
129 | content_length = resp.Content.Headers.ContentLength;
130 | }
131 | }
132 | }
133 | catch (Exception ex)
134 | {
135 | Log($"Http Error: {ex.Message}", LogLevel.Error, nameof(_get_content_length));
136 | if (i < http_retries) Log($"Retry({i + 1}): {url}", LogLevel.Info, nameof(_get_content_length));
137 | }
138 | }
139 | return content_length;
140 | }
141 | #endregion
142 |
143 | private string[] available_domains = new string[]
144 | {
145 | "lanzouw.com", // 鲁ICP备15001327号-7, 2021-09-02, SEO 排名最低
146 | "lanzoui.com", // 鲁ICP备15001327号-6, 2020-06-09, SEO 排名最低
147 | "lanzoux.com", // 鲁ICP备15001327号-5, 2020-06-09
148 | "lanzous.com", // 主域名, 备案异常, 部分地区已经无法访问
149 | };
150 |
151 | private string fix_url_domain(string url, int index = 0)
152 | {
153 | if (!url.Contains("lanzous.com")) return url;
154 | return url.Replace("lanzous.com", available_domains[index]);
155 | }
156 |
157 |
158 |
159 | #region 辅助类
160 | internal class UTF8EncodingStreamContent : StreamContent
161 | {
162 | string fileName;
163 |
164 | internal UTF8EncodingStreamContent(Stream content, string name, string fileName) : base(content)
165 | {
166 | this.fileName = fileName;
167 | var fn = new StringBuilder();
168 | foreach (var b in Encoding.UTF8.GetBytes(fileName))
169 | {
170 | fn.Append((char)b);
171 | }
172 | Headers.Add("Content-Type", "application/octet-stream");
173 | Headers.Add("Content-Disposition", $"form-data; name=\"{name}\"; filename=\"{fn}\"");
174 | }
175 |
176 | protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
177 | {
178 | #if UNITY_5_3_OR_NEWER
179 | stream.Position = 0;
180 | var header = new StreamReader(stream).ReadToEnd();
181 | var h = header.IndexOf("filename=\"") + "filename=\"".Length;
182 | var t = header.IndexOf("\"", h);
183 | var newHeader = header.Substring(0, h) + fileName + header.Substring(t);
184 | var bytes = Encoding.UTF8.GetBytes(newHeader);
185 | stream.Position = 0;
186 | stream.Write(bytes, 0, bytes.Length);
187 | #endif
188 | return base.SerializeToStreamAsync(stream, context);
189 | }
190 |
191 | protected override bool TryComputeLength(out long length)
192 | {
193 | return base.TryComputeLength(out length);
194 | }
195 | }
196 |
197 | internal class ProgressableStreamContent : HttpContent
198 | {
199 | private HttpContent content;
200 | private int bufferSize;
201 | private Action progress;
202 | private CancellationToken cancellationToken;
203 |
204 | internal ProgressableStreamContent(HttpContent content, int bufferSize,
205 | Action progress, CancellationToken cancellationToken)
206 | {
207 | this.content = content;
208 | this.bufferSize = bufferSize;
209 | this.progress = progress;
210 | this.cancellationToken = cancellationToken;
211 |
212 | foreach (var h in content.Headers)
213 | {
214 | Headers.Add(h.Key, h.Value);
215 | }
216 | }
217 |
218 | protected override Task SerializeToStreamAsync(Stream netStream, TransportContext context)
219 | {
220 | return Task.Run(async () =>
221 | {
222 | var buffer = new byte[bufferSize];
223 | TryComputeLength(out var total);
224 | var current = 0;
225 | using (var fileStream = await content.ReadAsStreamAsync())
226 | {
227 | while (!cancellationToken.IsCancellationRequested)
228 | {
229 | var length = await fileStream.ReadAsync(buffer, 0, bufferSize, cancellationToken);
230 | if (length == 0)
231 | break;
232 |
233 | await netStream.WriteAsync(buffer, 0, length);
234 | current += length;
235 |
236 | progress?.Invoke(current, total);
237 | }
238 | }
239 | });
240 | }
241 |
242 | protected override bool TryComputeLength(out long length)
243 | {
244 | length = content.Headers.ContentLength.GetValueOrDefault();
245 | return true;
246 | }
247 |
248 | protected override void Dispose(bool disposing)
249 | {
250 | if (disposing)
251 | {
252 | content.Dispose();
253 | }
254 | base.Dispose(disposing);
255 | }
256 | }
257 | #endregion
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/src/LanZouCloud/Code/LanZouCloud.Log.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace LanZouCloudAPI
4 | {
5 | public partial class LanZouCloud
6 | {
7 | public enum LogLevel
8 | {
9 | None = 0,
10 | Error = 1,
11 | Warning = 2,
12 | Info = 3,
13 | }
14 |
15 | private LogLevel _print_log_level = LogLevel.Error;
16 | private LogLevel _write_log_level = LogLevel.None;
17 |
18 | ///
19 | /// 设置日志等级
20 | ///
21 | ///
22 | public void SetLogLevel(LogLevel level)
23 | {
24 | this._print_log_level = level;
25 | this._write_log_level = level;
26 | }
27 |
28 | ///
29 | /// 设置日志等级
30 | ///
31 | /// 打印日志等级
32 | /// 写入文件日志等级
33 | public void SetLogLevel(LogLevel printLevel, LogLevel writeLevel)
34 | {
35 | this._print_log_level = printLevel;
36 | this._write_log_level = writeLevel;
37 | }
38 |
39 | private void LogInfo(string msg, string module)
40 | {
41 | Log(msg, LogLevel.Info, module);
42 | }
43 |
44 | private void LogResult(Result result, string module)
45 | {
46 | if (result.code != LanZouCode.SUCCESS)
47 | {
48 | Log(result.message, LogLevel.Error, module);
49 | }
50 | else
51 | {
52 | Log(result.message, LogLevel.Info, module);
53 | }
54 | }
55 |
56 | private void Log(object log, LogLevel level, string module)
57 | {
58 | if (level == LogLevel.None)
59 | {
60 | return;
61 | }
62 |
63 | if (_print_log_level < level && _write_log_level < level)
64 | {
65 | return;
66 | }
67 |
68 | // log format:
69 | // time|lanzou|level|module|log
70 | // example:
71 | // 11.22.03.456|LanZou|E|Login|login failed cause network error.
72 | var time = DateTime.Now.ToString("HH:mm:ss.fff");
73 | var _level = level.ToString().Substring(0, 1);
74 | var _max_module_lens = 16;
75 | if (module.Length > _max_module_lens) module = module.Substring(0, _max_module_lens);
76 | else if (module.Length < _max_module_lens) module = module + new string(' ', _max_module_lens - module.Length);
77 | var _log = $"{time}|LanZou|{_level}|{module}|{log}";
78 |
79 | if (_print_log_level >= level)
80 | {
81 | Print(_log, level);
82 | }
83 |
84 | if (_write_log_level >= level)
85 | {
86 | Write(_log, level);
87 | }
88 | }
89 |
90 | private void Print(string log, LogLevel level)
91 | {
92 | #if UNITY_5_3_OR_NEWER
93 | if (level == LogLevel.Info) UnityEngine.Debug.Log($"{log}");
94 | else if (level == LogLevel.Warning) UnityEngine.Debug.LogWarning($"{log}");
95 | else if (level == LogLevel.Error) UnityEngine.Debug.LogError($"{log}");
96 | #else
97 | Console.WriteLine($"{log}");
98 | #endif
99 | }
100 |
101 |
102 | private void Write(string log, LogLevel level)
103 | {
104 | #if UNITY_5_3_OR_NEWER
105 | // UnityEngine.Debug.LogError($"{log}");
106 | #else
107 | // Console.WriteLine($"{log}");
108 | #endif
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/LanZouCloud/Code/LanZouCloud.Utils.cs:
--------------------------------------------------------------------------------
1 | using LitJson;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.IO;
6 | using System.Text.RegularExpressions;
7 | using System.Threading.Tasks;
8 |
9 | namespace LanZouCloudAPI
10 | {
11 | public partial class LanZouCloud
12 | {
13 | ///
14 | /// 删除网页的注释
15 | ///
16 | ///
17 | ///
18 | private string remove_notes(string html)
19 | {
20 | // 去掉 html 里面的 // 和 注释,防止干扰正则匹配提取数据
21 | // 蓝奏云的前端程序员喜欢改完代码就把原来的代码注释掉,就直接推到生产环境了 =_=
22 | html = Regex.Replace(html, "|\\s+//\\s*.+", ""); // html 注释
23 | html = Regex.Replace(html, "(.+?[,;])\\s*//.+", "\\1"); // js 注释
24 | return html;
25 | }
26 |
27 | ///
28 | /// 判断是否为文件的分享链接
29 | ///
30 | ///
31 | ///
32 | private async Task is_share_url(string share_url)
33 | {
34 | var base_pat = "https?://[a-zA-Z0-9-]*?\\.?lanzou\\w.com/.+"; // 子域名可个性化设置或者不存在
35 | var user_pat = "https?://[a-zA-Z0-9-]*?\\.?lanzou\\w.com/i[a-zA-Z0-9]{5,}/?"; // 普通用户 URL 规则
36 | if (!Regex.IsMatch(share_url, base_pat))
37 | return false;
38 | if (Regex.IsMatch(share_url, user_pat))
39 | return true;
40 |
41 | // VIP 用户的 URL 很随意
42 | var html = await _get_text(share_url);
43 | if (string.IsNullOrEmpty(share_url))
44 | return false;
45 |
46 | html = remove_notes(html);
47 | if (Regex.Match(html, "class=\"fileinfo\"|id=\"file\"|文件描述").Success)
48 | return true;
49 | return false;
50 | }
51 |
52 | private string calc_acw_sc__v2(string html_text)
53 | {
54 | var arg1 = Regex.Match(html_text, "arg1='([0-9A-Z]+)'");
55 | var arg1str = "";
56 | if (arg1.Success)
57 | arg1str = arg1.Groups[1].Value;
58 | var acw_sc__v2 = hex_xor(unsbox(arg1str), "3000176000856006061501533003690027800375");
59 | return acw_sc__v2;
60 | }
61 |
62 | // 参考自 https://zhuanlan.zhihu.com/p/228507547
63 | private string unsbox(string str_arg)
64 | {
65 | int[] v1 = new int[]{15, 35, 29, 24, 33, 16, 1, 38, 10, 9, 19, 31, 40, 27, 22, 23,
66 | 25, 13, 6, 11, 39, 18, 20, 8, 14, 21, 32, 26, 2, 30, 7, 4, 17, 5, 3, 28, 34, 37, 12, 36 };
67 | var v2 = new string[v1.Length];
68 | for (int idx = 0; idx < str_arg.Length; idx++)
69 | {
70 | var v3 = str_arg[idx];
71 | for (int idx2 = 0; idx2 < v1.Length; idx2++)
72 | {
73 | if (v1[idx2] == idx + 1)
74 | v2[idx2] = v3.ToString();
75 | }
76 | }
77 | var res = string.Join("", v2);
78 | return res;
79 | }
80 |
81 | private string hex_xor(string str_arg, string args)
82 | {
83 | var res = "";
84 | for (int idx = 0; idx < Math.Min(str_arg.Length, args.Length); idx += 2)
85 | {
86 | var v1 = Convert.ToInt32(str_arg.Substring(idx, 2), 16);
87 | var v2 = Convert.ToInt32(args.Substring(idx, 2), 16);
88 | var v3 = $"{v1 ^ v2:X2}";
89 | res += v3;
90 | }
91 | return res;
92 | }
93 |
94 | // TODO: 时间格式化
95 |
96 | ///
97 | /// 输出格式化时间 DateTime
98 | ///
99 | ///
100 | ///
101 | private string time_format(string time_str)
102 | {
103 | //if '秒前' in time_str or '分钟前' in time_str or '小时前' in time_str:
104 | // return datetime.today().strftime('%Y-%m-%d')
105 | //elif '昨天' in time_str:
106 | // return (datetime.today() - timedelta(days = 1)).strftime('%Y-%m-%d')
107 | //elif '前天' in time_str:
108 | // return (datetime.today() - timedelta(days = 2)).strftime('%Y-%m-%d')
109 | //elif '天前' in time_str:
110 | // days = time_str.replace(' 天前', '')
111 | // return (datetime.today() - timedelta(days = int(days))).strftime('%Y-%m-%d')
112 | //else:
113 | // return time_str
114 | return time_str;
115 | }
116 |
117 | ///
118 | /// 去除非法字符
119 | ///
120 | ///
121 | private string name_format(string name)
122 | {
123 | // 去除其它字符集的空白符,去除重复空白字符
124 | name = name.Replace("\xa0", " ").Replace("\u3000", " ").Replace(" ", " ");
125 | return Regex.Replace(name, "[$%^!*<>)(+=`'\"/:;,?]", "");
126 | }
127 |
128 | private static readonly string _network_error_msg = "Network error, please retry later";
129 | private static readonly string _not_login_msg = "You are not login, please login and retry";
130 | private static readonly string _task_canceled_msg = "Task was canceled";
131 | private static readonly string _success_msg = "Success";
132 |
133 | ///
134 | /// 从返回JSON中获得 结果
135 | ///
136 | ///
137 | ///
138 | private Result _get_result(string text)
139 | {
140 | if (string.IsNullOrEmpty(text))
141 | {
142 | return new Result(LanZouCode.NETWORK_ERROR, _network_error_msg);
143 | }
144 | if (text.Contains("info\":\"login not"))
145 | {
146 | return new Result(LanZouCode.NOT_LOGIN, _not_login_msg);
147 | }
148 | if (!text.Contains("zt\":1") && !text.Contains("zt\":2"))
149 | {
150 | string _err;
151 | if (!text.Contains("info\":\"")) _err = text;
152 | else _err = JsonMapper.ToObject(text)["info"].ToString();
153 | return new Result(LanZouCode.FAILED, _err);
154 | }
155 | return new Result(LanZouCode.SUCCESS, _success_msg);
156 | }
157 |
158 | ///
159 | /// 构建 Post 数据,格式:key, value, key, value, ...
160 | ///
161 | ///
162 | ///
163 | private Dictionary _post_data(params string[] key_vals)
164 | {
165 | if (key_vals == null || key_vals.Length % 2 != 0)
166 | throw new ArgumentException("参数数量不匹配!");
167 | var data = new Dictionary();
168 | for (int i = 0; i < key_vals.Length; i += 2)
169 | {
170 | data.Add(key_vals[i], key_vals[i + 1]);
171 | }
172 | return data;
173 | }
174 |
175 | ///
176 | /// 如果文件存在,则给文件名添加序号
177 | ///
178 | ///
179 | ///
180 | private string _auto_rename(string file_path)
181 | {
182 | if (!File.Exists(file_path))
183 | return file_path;
184 | var fpath = Path.GetDirectoryName(file_path);
185 | var fname_no_ext = Path.GetFileNameWithoutExtension(file_path);
186 | var ext = Path.GetExtension(file_path);
187 | var count = 1;
188 | var fset = new HashSet();
189 | foreach (var f in Directory.GetFiles(fpath))
190 | {
191 | fset.Add(Path.GetFileName(f));
192 | }
193 | while (count < 99999)
194 | {
195 | var current = $"{fname_no_ext}({count}){ext}";
196 | if (!fset.Contains(current))
197 | {
198 | return Path.Combine(fpath, current).Replace("\\", "/");
199 | }
200 | count++;
201 | }
202 | throw new Exception("重复文件数量过多,或其他未知错误");
203 | }
204 |
205 |
206 | ///
207 | /// 允许上传的文件后缀名
208 | ///
209 | public static readonly List valid_suffix_list = new List()
210 | {
211 | "ppt", "xapk", "ke", "azw", "cpk", "gho", "dwg", "db", "docx", "deb", "e", "ttf", "xls", "bat",
212 | "crx", "rpm", "txf", "pdf", "apk", "ipa", "txt", "mobi", "osk", "dmg", "rp", "osz", "jar",
213 | "ttc", "z", "w3x", "xlsx", "ct", "rar", "mp3", "pptx", "mobileconfig", "epub",
214 | "imazingapp", "doc", "iso", "img", "appimage", "7z", "rplib", "lolgezi", "exe", "azw3", "zip",
215 | "conf", "tar", "dll", "flac", "xpa", "lua", "cad", "hwt", "accdb", "ce",
216 | "xmind", "enc", "bds", "bdi", "ssf", "it", "gz"
217 | };
218 |
219 | ///
220 | /// 检查文件名是否允许上传
221 | ///
222 | ///
223 | ///
224 | private bool is_ext_valid(string filename)
225 | {
226 | var ext = Path.GetExtension(filename).Substring(1);
227 | return valid_suffix_list.Contains(ext);
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src/LanZouCloud/Code/LanZouCloud.cs:
--------------------------------------------------------------------------------
1 | using LitJson;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Net;
6 | using System.Net.Http;
7 | using System.Text;
8 | using System.Text.RegularExpressions;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 |
12 | namespace LanZouCloudAPI
13 | {
14 | public partial class LanZouCloud
15 | {
16 | private int _chunk_size = 4096; // 上传或下载是的块大小
17 | private int _timeout = 15; // 每个请求的超时(不包含下载响应体的用时)
18 | private int _max_size = 100; // 单个文件大小上限 MB
19 | private string _host_url = "https://pan.lanzoui.com";
20 | private string _doupload_url = "https://pc.woozooo.com/doupload.php";
21 | private string _account_url = "https://pc.woozooo.com/account.php";
22 | // private string _mydisk_url = "https://pc.woozooo.com/mydisk.php";
23 | private string _proxy = null;
24 | private Dictionary _headers = new Dictionary()
25 | {
26 | { "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" },
27 | { "Referer", "https://pc.woozooo.com/mydisk.php" },
28 | { "Accept-Language", "zh-CN,zh;q=0.9" }, // 提取直连必需设置这个,否则拿不到数据
29 | };
30 |
31 |
32 | #region Private APIs(内部使用)
33 | ///
34 | /// 获取全部文件夹 id-name 列表,用于移动文件至新的文件夹
35 | ///
36 | ///
37 | private async Task GetMoveFolders()
38 | {
39 | LogInfo("Get move folders", nameof(GetMoveFolders));
40 | MoveFolderList result;
41 |
42 | // 这里 file_id 可以为任意值,不会对结果产生影响
43 | var folders = new Dictionary();
44 | folders.Add(-1, "LanZouCloud");
45 | var post_data = _post_data("task", $"{19}", "file_id", $"{-1}");
46 | var text = await _post_text(_doupload_url, post_data);
47 | var _res = _get_result(text);
48 |
49 | if (_res.code != LanZouCode.SUCCESS) // 获取失败或者网络异常
50 | {
51 | result = new MoveFolderList(_res.code, _res.message, folders);
52 | }
53 | else
54 | {
55 | var json = JsonMapper.ToObject(text);
56 | if (!json.ContainsKey("info") || json["info"] == null) // 新注册用户无数据, info=null
57 | {
58 | Log("New user has no move folders", LogLevel.Warning, nameof(GetMoveFolders));
59 | result = new MoveFolderList(LanZouCode.SUCCESS, _success_msg, folders);
60 | }
61 | else
62 | {
63 | var info = json["info"];
64 | foreach (var j_folder in info)
65 | {
66 | var folder = (JsonData)j_folder;
67 | var folder_id = long.Parse(folder["folder_id"].ToString());
68 | var folder_name = folder["folder_name"].ToString();
69 | folders.Add(folder_id, folder_name);
70 | }
71 |
72 | result = new MoveFolderList(LanZouCode.SUCCESS, _success_msg, folders);
73 | }
74 | }
75 |
76 | LogResult(result, nameof(GetMoveFolders));
77 | return result;
78 | }
79 |
80 | ///
81 | /// 重命名文件夹及其描述
82 | ///
83 | ///
84 | ///
85 | ///
86 | ///
87 | private async Task SetFolderInfo(long folder_id, string folder_name, string desc = "")
88 | {
89 | LogInfo($"Set folder info file id: {folder_id}, folder name: {folder_name}, description: {desc}", nameof(SetFolderInfo));
90 |
91 | // 不能用于重命名文件,id 无效仍然返回成功
92 | folder_name = name_format(folder_name);
93 | var post_data = _post_data("task", $"{4}", "folder_id", $"{folder_id}", "folder_name", $"{folder_name}", "folder_description", $"{desc}");
94 | var text = await _post_text(_doupload_url, post_data);
95 | var result = _get_result(text);
96 |
97 | LogResult(result, nameof(SetFolderInfo));
98 | return result;
99 | }
100 | #endregion
101 |
102 |
103 | #region Public APIs (需要登录)
104 | ///
105 | /// 设置单文件大小限制(会员用户可超过 100M)
106 | ///
107 | /// 最大文件大小(MB)
108 | ///
109 | public Result SetMaxSize(int max_size = 100)
110 | {
111 | LogInfo($"Set file max size: {max_size}", nameof(SetMaxSize));
112 |
113 | Result result;
114 | if (max_size < 100)
115 | {
116 | result = new Result(LanZouCode.FAILED, $"文件大小({max_size})不能小于100MB");
117 | }
118 | else
119 | {
120 | _max_size = max_size;
121 | result = new Result(LanZouCode.SUCCESS, _success_msg);
122 | }
123 |
124 | LogResult(result, nameof(SetMaxSize));
125 | return result;
126 | }
127 |
128 | ///
129 | /// 通过cookie登录,在浏览器中获得网站的Cookie。
130 | ///
131 | /// Cookie位置: woozooo.com -> Cookie -> ylogin
132 | /// Cookie位置: pc.woozooo.com -> Cookie -> phpdisk_info
133 | /// 是否验证 cookie 有效性
134 | ///
135 | public async Task Login(string ylogin, string phpdisk_info, bool validate = true)
136 | {
137 | var print_pinfo = !string.IsNullOrEmpty(phpdisk_info) && phpdisk_info.Length > 10
138 | ? phpdisk_info.Substring(0, 10) + "..."
139 | : phpdisk_info;
140 |
141 | LogInfo($"Login with cookie ylogin: {ylogin}, phpdisk_info: {print_pinfo}", nameof(Login));
142 |
143 | Result result;
144 | _set_cookie("woozooo.com", "ylogin", ylogin);
145 | _set_cookie("pc.woozooo.com", "phpdisk_info", phpdisk_info);
146 |
147 | if (!validate)
148 | {
149 | result = new Result(LanZouCode.SUCCESS, _success_msg);
150 | }
151 | else
152 | {
153 | var html = await _get_text(_account_url);
154 |
155 | if (string.IsNullOrEmpty(html))
156 | {
157 | result = _get_result(html);
158 | }
159 | else if (html.Contains("网盘用户登录"))
160 | {
161 | result = new Result(LanZouCode.FAILED, "登录失败,Cookie已过期或不存在。");
162 | }
163 | else
164 | {
165 | result = new Result(LanZouCode.SUCCESS, _success_msg);
166 | }
167 | }
168 |
169 | LogResult(result, nameof(Login));
170 | return result;
171 | }
172 |
173 | ///
174 | /// 登出
175 | ///
176 | ///
177 | public async Task Logout()
178 | {
179 | LogInfo("Logout", nameof(Logout));
180 |
181 | Result result;
182 | var html = await _get_text($"{_account_url}?action=logout");
183 |
184 | if (string.IsNullOrEmpty(html))
185 | {
186 | result = _get_result(html);
187 | }
188 | else if (html.Contains("退出系统成功"))
189 | {
190 | result = new Result(LanZouCode.SUCCESS, _success_msg);
191 | }
192 | else
193 | {
194 | result = new Result(LanZouCode.FAILED, "退出系统失败");
195 | }
196 |
197 | LogResult(result, nameof(Logout));
198 | return result;
199 | }
200 |
201 | ///
202 | /// 把文件放到回收站
203 | ///
204 | /// 文件ID
205 | ///
206 | public async Task DeleteFile(long file_id)
207 | {
208 | LogInfo($"Delete file of file id: {file_id}", nameof(DeleteFile));
209 | var post_data = _post_data("task", $"{6}", "file_id", $"{file_id}");
210 | var text = await _post_text(_doupload_url, post_data);
211 | var result = _get_result(text);
212 | LogResult(result, nameof(DeleteFile));
213 | return result;
214 | }
215 |
216 | ///
217 | /// 把文件夹放到回收站,必须是单层文件夹(无子文件夹的)
218 | ///
219 | ///
220 | ///
221 | ///
222 | public async Task DeleteFolder(long folder_id)
223 | {
224 | LogInfo($"Delete folder of folder id: {folder_id}", nameof(DeleteFolder));
225 | var post_data = _post_data("task", $"{3}", "folder_id", $"{folder_id}");
226 | var text = await _post_text(_doupload_url, post_data);
227 | var result = _get_result(text);
228 | LogResult(result, nameof(DeleteFolder));
229 | return result;
230 | }
231 |
232 | ///
233 | /// 获取文件列表
234 | /// 官方默认每页 18 条数据
235 | ///
236 | /// 文件夹ID,默认值 -1 表示根路径
237 | /// 开始页码,默认值 1 为起始页
238 | /// 获取页数,默认值 -1 表示所有
239 | ///
240 | public async Task GetFileList(long folder_id = -1, int page_begin = 1, int page_count = -1)
241 | {
242 | LogInfo($"Get file list of folder id: {folder_id}, begin page: {page_begin}, count: {page_count}", nameof(GetFileList));
243 |
244 | CloudFileList result;
245 | long page = page_begin;
246 | long page_end = page_count < 0 ? long.MaxValue : ((long)page_begin + page_count);
247 | var file_list = new List();
248 | while (page < page_end)
249 | {
250 | var post_data = _post_data("task", $"{5}", "folder_id", $"{folder_id}", "pg", $"{page}");
251 | var text = await _post_text(_doupload_url, post_data);
252 | var _res = _get_result(text);
253 | if (_res.code != LanZouCode.SUCCESS)
254 | {
255 | result = new CloudFileList(_res.code, _res.message, file_list);
256 | LogResult(result, nameof(GetFileList));
257 | return result;
258 | }
259 |
260 | var json = JsonMapper.ToObject(text);
261 |
262 | if (int.Parse(json["info"].ToString()) == 0) // 已经拿到了全部的文件信息
263 | break;
264 |
265 | foreach (var _json in json["text"])
266 | {
267 | var f_json = (JsonData)_json;
268 | file_list.Add(new CloudFile()
269 | {
270 | id = long.Parse(f_json["id"].ToString()), // 文件ID
271 | name = f_json["name_all"].ToString().Replace("&", "&"), // 文件名
272 | time = time_format((string)f_json["time"]), // 上传时间
273 | size = f_json["size"].ToString().Replace(",", ""), // 文件大小
274 | type = Path.GetExtension(f_json["name_all"].ToString()).Substring(1), // 文件类型
275 | downloads = int.Parse(f_json["downs"].ToString()), // 下载次数
276 | hasPassword = int.Parse(f_json["onof"].ToString()) == 1, // 是否存在提取码
277 | hasDescription = int.Parse(f_json["is_des"].ToString()) == 1, // 是否存在描述
278 | });
279 | }
280 |
281 | page += 1; // 下一页
282 | }
283 |
284 | result = new CloudFileList(LanZouCode.SUCCESS, _success_msg, file_list);
285 | LogResult(result, nameof(GetFileList));
286 | return result;
287 | }
288 |
289 | ///
290 | /// 获取子文件夹列表
291 | ///
292 | /// 文件夹ID,默认值 -1 表示根路径
293 | ///
294 | public async Task GetFolderList(long folder_id = -1)
295 | {
296 | LogInfo($"Get folder list of folder id: {folder_id}", nameof(GetFolderList));
297 | CloudFolderList result;
298 |
299 | var folder_list = new List();
300 | var post_data = _post_data("task", $"{47}", "folder_id", $"{folder_id}");
301 | var text = await _post_text(_doupload_url, post_data);
302 | var _res = _get_result(text);
303 |
304 | if (_res.code != LanZouCode.SUCCESS)
305 | {
306 | result = new CloudFolderList(_res.code, _res.message, folder_list);
307 | }
308 | else
309 | {
310 | var json = JsonMapper.ToObject(text);
311 |
312 | foreach (var _json in json["text"])
313 | {
314 | var f_json = (JsonData)_json;
315 | folder_list.Add(new CloudFolder()
316 | {
317 | id = long.Parse(f_json["fol_id"].ToString()),
318 | name = f_json["name"].ToString(),
319 | hasPassword = int.Parse(f_json["onof"].ToString()) == 1,
320 | description = f_json["folder_des"].ToString().Trim('[', ']'),
321 | });
322 | }
323 | result = new CloudFolderList(LanZouCode.SUCCESS, _success_msg, folder_list);
324 | }
325 |
326 | LogResult(result, nameof(GetFolderList));
327 | return result;
328 | }
329 |
330 | ///
331 | /// 通过文件ID,获取文件各种信息(包括下载直链)
332 | ///
333 | /// 文件ID
334 | ///
335 | public async Task GetFileInfo(long file_id)
336 | {
337 | LogInfo($"Get file info of file id: {file_id}", nameof(GetFileInfo));
338 |
339 | CloudFileInfo result = null;
340 |
341 | var _result = await GetFileShareInfo(file_id);
342 | if (_result.code != LanZouCode.SUCCESS)
343 | {
344 | result = new CloudFileInfo(_result.code, _result.message);
345 | }
346 | else
347 | {
348 | result = await GetFileInfoByUrl(_result.url, _result.password);
349 | }
350 |
351 | LogResult(result, nameof(GetFileInfo));
352 | return result;
353 | }
354 |
355 |
356 | ///
357 | /// 通过文件夹ID,获取文件夹及其子文件信息
358 | ///
359 | /// 文件夹ID
360 | /// 开始页码,默认值 1 为起始页
361 | /// 获取页数,默认值 -1 表示全部
362 | ///
363 | public async Task GetFolderInfo(long folder_id, int page_begin = 1, int page_count = -1)
364 | {
365 | LogInfo($"Get folder info of folder id: {folder_id}, begin page : {page_begin}, count: {page_count}", nameof(GetFolderInfo));
366 |
367 | CloudFolderInfo result;
368 |
369 | var _share = await GetFolderShareInfo(folder_id);
370 | if (_share.code != LanZouCode.SUCCESS)
371 | {
372 | result = new CloudFolderInfo(_share.code, _share.message);
373 | }
374 | else
375 | {
376 | result = await GetFolderInfoByUrl(_share.url, _share.password, page_begin, page_count);
377 | }
378 |
379 | LogResult(result, nameof(GetFolderInfo));
380 | return result;
381 | }
382 |
383 | ///
384 | /// 获取文件提取码、分享链接
385 | ///
386 | /// 文件ID
387 | ///
388 | public async Task GetFileShareInfo(long file_id)
389 | {
390 | LogInfo($"Get file share info of file id: {file_id}", nameof(GetFileShareInfo));
391 |
392 | ShareInfo result;
393 |
394 | var post_data = _post_data("task", $"{22}", "file_id", $"{file_id}");
395 |
396 | // 获取分享链接和密码用
397 | var text = await _post_text(_doupload_url, post_data);
398 | var _res = _get_result(text);
399 | if (_res.code != LanZouCode.SUCCESS)
400 | {
401 | result = new ShareInfo(LanZouCode.NETWORK_ERROR, _res.message);
402 | }
403 | else
404 | {
405 | var f_info = JsonMapper.ToObject(text)["info"];
406 |
407 | // 有效性校验
408 | if (f_info.ContainsKey("f_id") && f_info["f_id"].ToString() == "i")
409 | {
410 | result = new ShareInfo(LanZouCode.FAILED, "ID校验失败");
411 | }
412 | else
413 | {
414 | // onof=1 时,存在有效的提取码; onof=0 时不存在提取码,但是 pwd 字段还是有一个无效的随机密码
415 | var pwd = f_info["onof"].ToString() == "1" ? f_info["pwd"].ToString() : "";
416 | var url = f_info["is_newd"] + "/" + f_info["f_id"]; // 文件的分享链接需要拼凑
417 | var post_data_1 = _post_data("task", $"{12}", "file_id", $"{file_id}");
418 | var _text = await _post_text(_doupload_url, post_data_1); // 文件信息
419 | var __res = _get_result(_text);
420 | if (__res.code != LanZouCode.SUCCESS)
421 | {
422 | result = new ShareInfo(LanZouCode.NETWORK_ERROR, _res.message);
423 | }
424 | else
425 | {
426 | var file_info = JsonMapper.ToObject(_text);
427 | // 无后缀的文件名(获得后缀又要发送请求,没有就没有吧,尽可能减少请求数量)
428 | var name = file_info["text"].ToString();
429 | var desc = file_info["info"].ToString();
430 |
431 | result = new ShareInfo(LanZouCode.SUCCESS, _success_msg, name, url, desc, pwd);
432 | }
433 | }
434 | }
435 |
436 | LogResult(result, nameof(GetFileShareInfo));
437 | return result;
438 | }
439 |
440 | ///
441 | /// 获取文件夹提取码、分享链接
442 | ///
443 | /// 文件夹ID
444 | ///
445 | public async Task GetFolderShareInfo(long folder_id)
446 | {
447 | LogInfo($"Get folder share info of folder id: {folder_id}", nameof(GetFolderShareInfo));
448 |
449 | ShareInfo result;
450 |
451 | var post_data = _post_data("task", $"{18}", "folder_id", $"{folder_id}");
452 |
453 | // 获取分享链接和密码用
454 | var text = await _post_text(_doupload_url, post_data);
455 | var _res = _get_result(text);
456 | if (_res.code != LanZouCode.SUCCESS)
457 | {
458 | result = new ShareInfo(LanZouCode.NETWORK_ERROR, _res.message);
459 | }
460 | else
461 | {
462 | var f_info = JsonMapper.ToObject(text)["info"];
463 |
464 | // 有效性校验
465 | if (f_info.ContainsKey("name") && string.IsNullOrEmpty(f_info["name"].ToString()))
466 | {
467 | result = new ShareInfo(LanZouCode.FAILED, "Name校验失败");
468 | }
469 | else
470 | {
471 | // onof=1 时,存在有效的提取码; onof=0 时不存在提取码,但是 pwd 字段还是有一个无效的随机密码
472 | var pwd = f_info["onof"].ToString() == "1" ? f_info["pwd"].ToString() : "";
473 | var url = f_info["new_url"].ToString(); // 文件夹的分享链接可以直接拿到
474 | var name = f_info["name"].ToString(); // 文件夹名
475 | var desc = f_info["des"].ToString(); // 文件夹描述
476 | result = new ShareInfo(LanZouCode.SUCCESS, _success_msg, name, url, desc, pwd);
477 | }
478 | }
479 |
480 | LogResult(result, nameof(GetFolderShareInfo));
481 | return result;
482 | }
483 |
484 | ///
485 | /// 设置文件的提取码
486 | /// id 无效或者 id 类型不对应仍然返回成功 :(
487 | /// 文件提取码 2 - 6 位
488 | ///
489 | /// 文件ID
490 | /// 提取码
491 | ///
492 | public async Task SetFilePassword(long file_id, string pwd = null)
493 | {
494 | LogInfo($"Set file password of file id: {file_id}", nameof(SetFilePassword));
495 |
496 | var pwd_status = string.IsNullOrEmpty(pwd) ? 0 : 1; // 是否开启密码
497 | var post_data = _post_data("task", $"{23}", "file_id", $"{file_id}", "shows", $"{pwd_status}", "shownames", $"{pwd}");
498 | var text = await _post_text(_doupload_url, post_data);
499 | var result = _get_result(text);
500 |
501 | LogResult(result, nameof(SetFilePassword));
502 | return result;
503 | }
504 |
505 | ///
506 | /// 设置文件夹的提取码, 现在非会员用户不允许关闭提取码
507 | /// id 无效或者 id 类型不对应仍然返回成功 :(
508 | /// 文件夹提取码长度 0 - 12 位
509 | ///
510 | /// 文件夹ID
511 | /// 提取码
512 | ///
513 | public async Task SetFolderPassword(long folder_id, string pwd = null)
514 | {
515 | LogInfo($"Set folder password of folder id: {folder_id}", nameof(SetFolderPassword));
516 |
517 | var pwd_status = string.IsNullOrEmpty(pwd) ? 0 : 1; // 是否开启密码
518 | var post_data = _post_data("task", $"{16}", "folder_id", $"{folder_id}", "shows", $"{pwd_status}", "shownames", $"{pwd}");
519 | var text = await _post_text(_doupload_url, post_data);
520 | var result = _get_result(text);
521 |
522 | LogResult(result, nameof(SetFolderPassword));
523 | return result;
524 | }
525 |
526 | ///
527 | /// 创建文件夹(同时设置描述)
528 | ///
529 | /// 文件夹名
530 | /// 父级文件夹ID
531 | /// 文件夹描述(可空)
532 | ///
533 | public async Task CreateFolder(string folder_name, long parent_id = -1, string description = "")
534 | {
535 | LogInfo($"Create folder named: {folder_name} in folder id: {parent_id}, description: {description}", nameof(CreateFolder));
536 |
537 | CreateFolderInfo result = null;
538 | CloudFolder exist_folder = null;
539 | MoveFolderList raw_move_folder_list = null;
540 |
541 | folder_name = folder_name.Replace(' ', '_'); // 文件夹名称不能包含空格
542 | folder_name = name_format(folder_name); // 去除非法字符
543 |
544 | var folder_list = await GetFolderList(parent_id);
545 | if (folder_list.code != LanZouCode.SUCCESS)
546 | {
547 | result = new CreateFolderInfo(folder_list.code, folder_list.message);
548 | }
549 | else if ((exist_folder = folder_list.folders.Find(a => a.name == folder_name)) != null)
550 | {
551 | // 如果文件夹已经存在,直接返回
552 | result = new CreateFolderInfo(LanZouCode.SUCCESS, _success_msg, exist_folder.id, exist_folder.name, exist_folder.description);
553 | }
554 | else if ((raw_move_folder_list = await GetMoveFolders()).code != LanZouCode.SUCCESS)
555 | {
556 | result = new CreateFolderInfo(raw_move_folder_list.code, raw_move_folder_list.message);
557 | }
558 |
559 | if (result != null)
560 | {
561 | LogResult(result, nameof(CreateFolder));
562 | return result;
563 | }
564 |
565 | var post_data = _post_data("task", $"{2}", "parent_id", $"{parent_id}", "folder_name", $"{folder_name}", "folder_description", $"{description}");
566 | var text = await _post_text(_doupload_url, post_data); // 创建文件夹
567 | var _res = _get_result(text);
568 | if (_res.code != LanZouCode.SUCCESS)
569 | {
570 | result = new CreateFolderInfo(_res.code, _res.message);
571 | LogResult(result, nameof(CreateFolder));
572 | return result;
573 | }
574 |
575 | // 允许在不同路径创建同名文件夹, 移动时可通过 get_move_paths() 区分
576 | var now_move_folder_list = await GetMoveFolders();
577 | if (now_move_folder_list.code != LanZouCode.SUCCESS)
578 | {
579 | result = new CreateFolderInfo(now_move_folder_list.code, now_move_folder_list.message);
580 | LogResult(result, nameof(CreateFolder));
581 | return result;
582 | }
583 |
584 | foreach (var kv in now_move_folder_list.folders)
585 | {
586 | if (!raw_move_folder_list.folders.ContainsKey(kv.Key)) // 不在原始列表中,即新增文件夹
587 | {
588 | // 创建文件夹成功
589 | result = new CreateFolderInfo(LanZouCode.SUCCESS, _success_msg, kv.Key, kv.Value, description);
590 | LogResult(result, nameof(CreateFolder));
591 | return result;
592 | }
593 | }
594 |
595 | // 没有找到匹配的文件夹,创建失败
596 | result = new CreateFolderInfo(LanZouCode.FAILED, $"Move folders no match: {folder_name}");
597 | LogResult(result, nameof(CreateFolder));
598 | return result;
599 | }
600 |
601 | ///
602 | /// 设置文件描述
603 | ///
604 | /// 文件ID
605 | /// 文件描述(一旦设置了值,就不能再设置为空)
606 | ///
607 | public async Task SetFileDescription(long file_id, string description = "")
608 | {
609 | LogInfo($"Set file description: {description} of file id: {file_id}", nameof(SetFileDescription));
610 |
611 | // 文件描述一旦设置了值,就不能再设置为空
612 | var post_data = _post_data("task", $"{11}", "file_id", $"{file_id}", "desc", $"{description}");
613 | var text = await _post_text(_doupload_url, post_data);
614 | var result = _get_result(text);
615 |
616 | LogResult(result, nameof(SetFileDescription));
617 | return result;
618 | }
619 |
620 | ///
621 | /// 设置文件夹描述
622 | ///
623 | /// 文件夹ID
624 | /// 文件夹描述(可以置空)
625 | ///
626 | public async Task SetFolderDescription(long folder_id, string description = "")
627 | {
628 | LogInfo($"Set folder description: {description} of folder id: {folder_id}", nameof(SetFolderDescription));
629 |
630 | Result result;
631 | var share = await GetFolderShareInfo(folder_id);
632 | if (share.code != LanZouCode.SUCCESS)
633 | {
634 | result = new Result(share.code, share.message);
635 | }
636 | else
637 | {
638 | result = await SetFolderInfo(folder_id, share.name, description);
639 | }
640 |
641 | LogResult(result, nameof(SetFolderDescription));
642 | return result;
643 | }
644 |
645 | ///
646 | /// 允许会员重命名文件(无法修后缀名)
647 | ///
648 | /// 文件ID
649 | /// 文件名
650 | ///
651 | public async Task RenameFile(long file_id, string filename)
652 | {
653 | LogInfo($"Rename file to: {filename} of file id: {file_id}", nameof(RenameFile));
654 |
655 | // 重命名文件要开会员
656 | var post_data = _post_data("task", $"{46}", "file_id", $"{file_id}", "file_name", $"{name_format(filename)}", "type", $"{2}");
657 | var text = await _post_text(_doupload_url, post_data);
658 | var result = _get_result(text);
659 |
660 | LogResult(result, nameof(RenameFile));
661 | return result;
662 | }
663 |
664 | ///
665 | /// 重命名文件夹
666 | ///
667 | /// 文件夹ID
668 | /// 文件夹名称
669 | ///
670 | public async Task RenameFolder(long folder_id, string folder_name)
671 | {
672 | LogInfo($"Rename foler to: {folder_name} of folder id: {folder_id}", nameof(RenameFolder));
673 |
674 | Result result;
675 |
676 | var share = await GetFolderShareInfo(folder_id);
677 | if (share.code != LanZouCode.SUCCESS)
678 | {
679 | result = new Result(share.code, share.message);
680 | }
681 | else
682 | {
683 | result = await SetFolderInfo(folder_id, folder_name, share.description);
684 | }
685 |
686 | LogResult(result, nameof(RenameFolder));
687 | return result;
688 | }
689 |
690 | ///
691 | /// 移动文件到指定文件夹
692 | ///
693 | ///
694 | ///
695 | ///
696 | public async Task MoveFile(long file_id, long parent_folder_id = -1)
697 | {
698 | LogInfo($"MoveFile file to folder id: {parent_folder_id} of file id: {file_id}", nameof(MoveFile));
699 |
700 | // 移动回收站文件也返回成功(实际上行不通) (+_+)?
701 | var post_data = _post_data("task", $"{20}", "file_id", $"{file_id}", "folder_id", $"{parent_folder_id}");
702 | var text = await _post_text(_doupload_url, post_data);
703 | var result = _get_result(text);
704 |
705 | LogResult(result, nameof(MoveFile));
706 | return result;
707 | }
708 |
709 | ///
710 | /// 移动文件夹(官方并没有直接支持此功能)
711 | /// 这里只允许移动单层文件夹(即没有子文件夹)
712 | /// 注意【文件夹ID】会发生变化,同时【分享链接】也会发生变化
713 | /// 实现方式就是创建新文件夹,并依次移动子文件
714 | ///
715 | ///
716 | ///
717 | ///
718 | public async Task MoveFolder(long folder_id, long parent_folder_id = -1)
719 | {
720 | LogInfo($"MoveFile folder to folder id: {parent_folder_id} of foler id: {folder_id}", nameof(MoveFolder));
721 |
722 | MoveFolderInfo result = null;
723 | MoveFolderList move_folder_list = null;
724 | CloudFolderList sub_folder_list = null;
725 | ShareInfo share = null;
726 | CreateFolderInfo new_folder = null;
727 | Result setpwd = null;
728 | CloudFileList file_list = null;
729 | string folder_name = null;
730 |
731 | // 禁止移动文件夹到自身,禁止移动到 -2 这样的文件夹(文件还在,但是从此不可见)
732 | if (folder_id == parent_folder_id || parent_folder_id < -1)
733 | {
734 | result = new MoveFolderInfo(LanZouCode.FAILED, $"Invalid parent folder id: {parent_folder_id}");
735 | }
736 | else if ((move_folder_list = await GetMoveFolders()).code != LanZouCode.SUCCESS)
737 | {
738 | result = new MoveFolderInfo(move_folder_list.code, move_folder_list.message);
739 | }
740 | else if (!move_folder_list.folders.TryGetValue(folder_id, out folder_name))
741 | {
742 | result = new MoveFolderInfo(LanZouCode.FAILED, $"Not found folder id: {folder_id}");
743 | }
744 | else if ((sub_folder_list = await GetFolderList(folder_id)).code != LanZouCode.SUCCESS)
745 | {
746 | result = new MoveFolderInfo(sub_folder_list.code, sub_folder_list.message);
747 | }
748 | else if (sub_folder_list.folders.Count > 0)
749 | {
750 | // 存在子文件夹,禁止移动,递归操作可能会产生大量请求,这里只允许移动单层文件夹
751 | result = new MoveFolderInfo(LanZouCode.FAILED, $"Found subdirectory in folder id: {folder_id}");
752 | }
753 | else if ((share = await GetFolderShareInfo(folder_id)).code != LanZouCode.SUCCESS)
754 | {
755 | // 在目标文件夹下创建同名文件夹
756 | result = new MoveFolderInfo(share.code, share.message);
757 | }
758 | else if ((new_folder = await CreateFolder(folder_name, parent_folder_id, share.description))
759 | .code != LanZouCode.SUCCESS)
760 | {
761 | result = new MoveFolderInfo(new_folder.code, new_folder.message);
762 | }
763 | else if (new_folder.id == folder_id)
764 | {
765 | // 不可以 移动文件夹 到同一目录
766 | result = new MoveFolderInfo(LanZouCode.FAILED, $"Create Folder is same id: {folder_id}");
767 | }
768 | else if ((setpwd = await SetFolderPassword(new_folder.id, share.password)).code != LanZouCode.SUCCESS)
769 | {
770 | // 保持密码一致
771 | result = new MoveFolderInfo(setpwd.code, setpwd.message);
772 | }
773 | else if ((file_list = await GetFileList(folder_id)).code != LanZouCode.SUCCESS)
774 | {
775 | result = new MoveFolderInfo(file_list.code, file_list.message);
776 | }
777 |
778 | // 以上步骤失败,直接返回
779 | if (result != null)
780 | {
781 | LogResult(result, nameof(MoveFolder));
782 | return result;
783 | }
784 |
785 | // 移动子文件至新目录下
786 | foreach (var file in file_list.files)
787 | {
788 | var moveFile = await MoveFile(file.id, new_folder.id);
789 | if (moveFile.code != LanZouCode.SUCCESS)
790 | {
791 | // 任意文件移动失败,直接返回
792 | result = new MoveFolderInfo(moveFile.code, moveFile.message);
793 | LogResult(result, nameof(MoveFolder));
794 | return result;
795 | }
796 | }
797 |
798 | // 全部移动完成后删除原文件夹
799 | var del = await DeleteFolder(folder_id);
800 | if (del.code != LanZouCode.SUCCESS)
801 | {
802 | result = new MoveFolderInfo(del.code, del.message);
803 | }
804 | else
805 | {
806 | result = new MoveFolderInfo(LanZouCode.SUCCESS, _success_msg, new_folder.id, new_folder.name, new_folder.description);
807 | }
808 |
809 | LogResult(result, nameof(MoveFolder));
810 | return result;
811 | }
812 |
813 | ///
814 | /// 登录用户通过id下载文件(无需提取码)
815 | ///
816 | /// 文件ID
817 | /// 保存到本地文件夹路径
818 | /// 自定义文件名
819 | /// 文件已存在时是否强制覆盖
820 | /// 进度
821 | ///
822 | public async Task DownloadFile(long file_id, string save_dir, string custom_filename = null,
823 | bool overwrite = false, IProgress progress = null,
824 | CancellationToken cancellationToken = default(CancellationToken))
825 | {
826 | save_dir = Path.GetFullPath(save_dir);
827 | save_dir = save_dir.Replace("\\", "/");
828 |
829 | LogInfo($"Download file of file id: {file_id}, save to: {save_dir}, overwrire: {overwrite}", nameof(DownloadFile));
830 |
831 | DownloadInfo result;
832 | var share = await GetFileShareInfo(file_id);
833 | if (share.code != LanZouCode.SUCCESS)
834 | {
835 | result = new DownloadInfo(share.code, share.message);
836 | }
837 | else
838 | {
839 | result = await DownloadFileByUrl(share.url, save_dir, custom_filename, share.password, overwrite, progress, cancellationToken);
840 | }
841 |
842 | LogResult(result, nameof(DownloadFile));
843 | return result;
844 | }
845 |
846 | ///
847 | /// 上传不超过 100MB 的文件
848 | ///
849 | /// 文件路径
850 | /// 自定义上传文件名
851 | /// 上传至文件夹ID
852 | /// 是否覆盖云端已存在的同名文件
853 | /// 进度
854 | public async Task UploadFile(string file_path, string custom_filename = null, long folder_id = -1, bool overwrite = false,
855 | IProgress progress = null, CancellationToken cancellationToken = default(CancellationToken))
856 | {
857 | file_path = Path.GetFullPath(file_path);
858 | file_path = file_path.Replace("\\", "/");
859 |
860 | LogInfo($"Upload file: {file_path} to folder id: {folder_id}, overwrire: {overwrite}", nameof(UploadFile));
861 |
862 | UploadInfo result = null;
863 |
864 | var filename = name_format(custom_filename ?? Path.GetFileName(file_path));
865 |
866 | if (!File.Exists(file_path))
867 | {
868 | result = new UploadInfo(LanZouCode.PATH_ERROR, $"File not found: {file_path}", filename, file_path);
869 | LogResult(result, nameof(UploadFile));
870 | return result;
871 | }
872 |
873 | var file_size = new FileInfo(file_path).Length;
874 |
875 | var p_start = new ProgressInfo(ProgressState.Start, filename, 0, file_size);
876 | progress?.Report(p_start);
877 |
878 | if (file_size > _max_size * 1024 * 1024)
879 | {
880 | result = new UploadInfo(LanZouCode.OFFICIAL_LIMITED, $"上传超过最大文件大小({_max_size}MB): {file_path}", filename, file_path);
881 | }
882 | else if (!is_ext_valid(filename))
883 | {
884 | // 不允许上传的格式
885 | result = new UploadInfo(LanZouCode.OFFICIAL_LIMITED, $"文件后缀名不符合官方限制: {file_path}", filename, file_path);
886 | }
887 |
888 | // 粗略判断,直接返回
889 | if (result != null)
890 | {
891 | LogResult(result, nameof(UploadFile));
892 | return result;
893 | }
894 |
895 | // 文件已经存在同名文件就删除
896 | if (overwrite)
897 | {
898 | var file_list = await GetFileList(folder_id);
899 | if (file_list.code != LanZouCode.SUCCESS)
900 | {
901 | result = new UploadInfo(file_list.code, file_list.message, filename, file_path);
902 | LogResult(result, nameof(UploadFile));
903 | return result;
904 | }
905 |
906 | var same_files = file_list.files.FindAll(a => a.name == filename);
907 | foreach (var file in same_files)
908 | {
909 | Log($"Upload {filename}, overwrite file id: {file.id}", LogLevel.Info, nameof(UploadFile));
910 | var del = await DeleteFile(file.id);
911 | if (del.code != LanZouCode.SUCCESS)
912 | {
913 | // 删除失败,只输出警告信息,不终止流程
914 | Log($"Upload {filename}, overwrite file id: {file.id} failed.", LogLevel.Warning, nameof(UploadFile));
915 | }
916 | }
917 | }
918 |
919 | var p_ready = new ProgressInfo(ProgressState.Ready, filename, 0, file_size);
920 | progress?.Report(p_ready);
921 |
922 | Log($"Upload stream begin, file: {file_path} to folder id: {folder_id}", LogLevel.Info, nameof(UploadFile));
923 |
924 | string text = null;
925 |
926 | var upload_url = "https://pc.woozooo.com/fileup.php";
927 |
928 | bool isCanceled = false;
929 | bool isUploadSuccess = false;
930 |
931 | for (int i = 0; i < http_retries; i++)
932 | {
933 | try
934 | {
935 | using (var fileStream = new FileStream(file_path, FileMode.Open, FileAccess.Read, FileShare.Read))
936 | {
937 | var _content = new MultipartFormDataContent();
938 |
939 | _content.Add(new StringContent("1"), "task");
940 | _content.Add(new StringContent(folder_id.ToString()), "folder_id");
941 | _content.Add(new StringContent("WU_FILE_0"), "id");
942 | _content.Add(new StringContent(filename, Encoding.UTF8), "name");
943 | _content.Add(new UTF8EncodingStreamContent(fileStream, "upload_file", filename));
944 |
945 | var p_uploading = new ProgressInfo(ProgressState.Progressing, filename, 0, file_size);
946 | var content = new ProgressableStreamContent(_content, _chunk_size, (_current, _total) =>
947 | {
948 | p_uploading.current = _current;
949 | p_uploading.total = _total;
950 | progress?.Report(p_uploading);
951 | }, cancellationToken);
952 |
953 | using (content)
954 | {
955 | using (var client = _get_client(null, 3600))
956 | {
957 | using (var resp = await client.PostAsync(upload_url, content, cancellationToken))
958 | {
959 | resp.EnsureSuccessStatusCode();
960 | text = await resp.Content.ReadAsStringAsync();
961 | }
962 | }
963 | }
964 | }
965 | isUploadSuccess = true;
966 | break;
967 | }
968 | catch (TaskCanceledException)
969 | {
970 | isCanceled = true;
971 | Log($"Http Canceled", LogLevel.Info, nameof(UploadFile));
972 | break;
973 | }
974 | catch (Exception ex)
975 | {
976 | Log($"Http Error: {ex.Message}", LogLevel.Error, nameof(UploadFile));
977 | if (i < http_retries) Log($"Retry({i + 1}): {upload_url}", LogLevel.Info, nameof(UploadFile));
978 | }
979 | }
980 |
981 | Result _res = null;
982 | if (!isUploadSuccess && isCanceled)
983 | {
984 | result = new UploadInfo(LanZouCode.TASK_CANCELED, _task_canceled_msg, filename, file_path);
985 | }
986 | else if ((_res = _get_result(text)).code != LanZouCode.SUCCESS)
987 | {
988 | result = new UploadInfo(_res.code, _res.message, filename, file_path);
989 | }
990 | else
991 | {
992 | var json = JsonMapper.ToObject(text);
993 | var file_id = long.Parse(json["text"][0]["id"].ToString());
994 | var f_id = json["text"][0]["f_id"].ToString();
995 | var is_newd = json["text"][0]["is_newd"].ToString();
996 | var share_url = is_newd + "/" + f_id;
997 |
998 | var p_finish = new ProgressInfo(ProgressState.Finish, filename, file_size, file_size);
999 | progress?.Report(p_finish);
1000 |
1001 | await Task.Yield(); // 保证 progress report 到达
1002 | await Task.Yield(); // 保证 progress report 到达
1003 |
1004 | result = new UploadInfo(LanZouCode.SUCCESS, _success_msg, filename, file_path, file_id, share_url);
1005 | }
1006 |
1007 | LogResult(result, nameof(UploadFile));
1008 | return result;
1009 | }
1010 |
1011 | #endregion
1012 |
1013 |
1014 | #region Public APIs (无需登录)
1015 |
1016 | ///
1017 | /// 通过分享链接,下载文件(需提取码)
1018 | /// 此接口无需登录
1019 | ///
1020 | /// 分享链接
1021 | /// 保存至本地的文件夹路径
1022 | /// 自定义文件名
1023 | /// 提取码
1024 | /// 文件已存在时是否强制覆盖
1025 | /// 用于显示下载进度
1026 | ///
1027 | public async Task DownloadFileByUrl(string share_url, string save_dir, string custom_filename = null,
1028 | string pwd = null, bool overwrite = false, IProgress progress = null,
1029 | CancellationToken cancellationToken = default(CancellationToken))
1030 | {
1031 | save_dir = Path.GetFullPath(save_dir);
1032 | save_dir = save_dir.Replace("\\", "/");
1033 |
1034 | LogInfo($"Download file of url: {share_url}, save to: {save_dir}, overwrire: {overwrite}", nameof(DownloadFileByUrl));
1035 |
1036 | DownloadInfo result = null;
1037 | CloudFileInfo file_info = null;
1038 |
1039 | var p_start = new ProgressInfo(ProgressState.Start);
1040 | progress?.Report(p_start);
1041 |
1042 | if ((file_info = await GetFileInfoByUrl(share_url, pwd)).code != LanZouCode.SUCCESS)
1043 | {
1044 | result = new DownloadInfo(file_info.code, file_info.message, share_url);
1045 | }
1046 |
1047 | if (result != null)
1048 | {
1049 | LogResult(result, nameof(DownloadFileByUrl));
1050 | return result;
1051 | }
1052 |
1053 | // 只请求头
1054 | var _con_len = (await _get_content_length(file_info.durl));
1055 |
1056 | // 对于 txt 文件, 可能出现没有 Content-Length 的情况
1057 | // 此时文件需要下载一次才会出现 Content-Length
1058 | // 这时候我们先读取一点数据, 再尝试获取一次, 通常只需读取 1 字节数据
1059 | if (_con_len == null)
1060 | {
1061 | Log("Not found Content-Length in response headers", LogLevel.Warning, nameof(DownloadFileByUrl));
1062 |
1063 | for (int i = 0; i < http_retries; i++)
1064 | {
1065 | try
1066 | {
1067 | // 请求内容
1068 | using (var client = _get_client())
1069 | {
1070 | using (var _stream = await client.GetStreamAsync(file_info.durl))
1071 | {
1072 | var _buffer = new byte[1];
1073 | var max_retries = 5; // 5 次拿不到就算了
1074 |
1075 | while (_con_len == null && max_retries > 0
1076 | && !cancellationToken.IsCancellationRequested)
1077 | {
1078 | max_retries -= 1;
1079 | await _stream.ReadAsync(_buffer, 0, 1, cancellationToken);
1080 |
1081 | // 再请求一次试试,只请求头
1082 | _con_len = (await _get_content_length(file_info.durl));
1083 | Log($"Retry to get Content-Length: {_con_len}", LogLevel.Info, nameof(DownloadFileByUrl));
1084 | }
1085 | }
1086 | }
1087 | break;
1088 | }
1089 | catch (TaskCanceledException)
1090 | {
1091 | Log($"Http Canceled", LogLevel.Info, nameof(DownloadFileByUrl));
1092 | break;
1093 | }
1094 | catch (Exception ex)
1095 | {
1096 | Log($"Http Error: {ex.Message}", LogLevel.Error, nameof(DownloadFileByUrl));
1097 | if (i < http_retries) Log($"Retry({i + 1}): {file_info.durl}", LogLevel.Info, nameof(DownloadFileByUrl));
1098 | }
1099 | }
1100 | }
1101 |
1102 | if (cancellationToken.IsCancellationRequested)
1103 | {
1104 | result = new DownloadInfo(LanZouCode.TASK_CANCELED, _task_canceled_msg);
1105 | LogResult(result, nameof(DownloadFileByUrl));
1106 | return result;
1107 | }
1108 |
1109 | // 应该不会出现这种情况
1110 | if (_con_len == null)
1111 | {
1112 | result = new DownloadInfo(LanZouCode.FAILED, "Not found Content-Length", share_url, file_info.name);
1113 | LogResult(result, nameof(DownloadFileByUrl));
1114 | return result;
1115 | }
1116 |
1117 | var content_length = _con_len.GetValueOrDefault();
1118 |
1119 | // 如果本地存在同名文件且设置了 overwrite, 则覆盖原文件
1120 | // 否则修改下载文件路径, 自动在文件名后加序号
1121 | var file_path = Path.Combine(save_dir, file_info.name);
1122 | file_path = Path.GetFullPath(file_path);
1123 | file_path = file_path.Replace("\\", "/");
1124 |
1125 | if (File.Exists(file_path) && !overwrite)
1126 | {
1127 | file_path = _auto_rename(file_path); // 自动重命名文件
1128 | Log($"File has already exists, auto rename to {file_path}", LogLevel.Info, nameof(DownloadFileByUrl));
1129 | }
1130 |
1131 | var tmp_file_path = file_path + ".download"; // 正在下载中的文件名
1132 | Log($"Save file to tmp path: {tmp_file_path}", LogLevel.Info, nameof(DownloadFileByUrl));
1133 |
1134 | // 支持断点续传下载
1135 | long now_size = 0;
1136 | bool is_continue = false;
1137 | bool is_downloaded = false;
1138 | if (File.Exists(tmp_file_path))
1139 | {
1140 | now_size = new FileInfo(tmp_file_path).Length; // 本地已经下载的文件大小
1141 | is_continue = true;
1142 | if (now_size > content_length) // 大小错误,删除重新下载
1143 | {
1144 | File.Delete(tmp_file_path);
1145 | now_size = 0;
1146 | is_continue = false;
1147 | }
1148 | else if (now_size == content_length) // 已经下载完成,只是未改后缀名
1149 | {
1150 | is_downloaded = true;
1151 | }
1152 | }
1153 |
1154 | var filename = Path.GetFileName(file_path);
1155 |
1156 | var p_ready = new ProgressInfo(ProgressState.Ready, filename, now_size, content_length);
1157 | progress?.Report(p_ready);
1158 | bool isDownloadSuccess = false;
1159 | bool isCanceled = false;
1160 |
1161 | if (is_downloaded)
1162 | {
1163 | Log($"Has already downloaded: {tmp_file_path}", LogLevel.Info, nameof(DownloadFileByUrl));
1164 | var p_downloading = new ProgressInfo(ProgressState.Progressing, filename, now_size, content_length);
1165 | progress?.Report(p_downloading);
1166 | isDownloadSuccess = true;
1167 | }
1168 | else
1169 | {
1170 | await Task.Run(async () =>
1171 | {
1172 | var headers = new Dictionary(_headers);
1173 | headers.Add("Range", $"bytes={now_size}-");
1174 | int chunk_size = _chunk_size;
1175 | var chunk = new byte[chunk_size];
1176 | var p_downloading = new ProgressInfo(ProgressState.Progressing, filename, now_size, content_length);
1177 | for (int i = 0; i < http_retries; i++)
1178 | {
1179 | try
1180 | {
1181 | using (var client = _get_client(headers))
1182 | {
1183 | using (var netStream = await client.GetStreamAsync(file_info.durl))
1184 | {
1185 | if (!Directory.Exists(save_dir))
1186 | {
1187 | Log($"Save dir not exsit, auto create: {save_dir}", LogLevel.Info, nameof(DownloadFileByUrl));
1188 | Directory.CreateDirectory(save_dir);
1189 | }
1190 |
1191 | using (var fileStream = new FileStream(tmp_file_path, FileMode.Append,
1192 | FileAccess.Write, FileShare.Read, chunk_size))
1193 | {
1194 | while (!cancellationToken.IsCancellationRequested)
1195 | {
1196 | var readLength = await netStream.ReadAsync(chunk, 0, chunk_size, cancellationToken);
1197 | if (readLength == 0)
1198 | break;
1199 |
1200 | await fileStream.WriteAsync(chunk, 0, readLength);
1201 | now_size += readLength;
1202 |
1203 | p_downloading.current = now_size;
1204 | progress?.Report(p_downloading);
1205 | }
1206 | }
1207 | }
1208 | }
1209 |
1210 | isDownloadSuccess = true;
1211 | break;
1212 | }
1213 | catch (TaskCanceledException)
1214 | {
1215 | isCanceled = true;
1216 | Log($"Http Canceled", LogLevel.Info, nameof(DownloadFileByUrl));
1217 | break;
1218 | }
1219 | catch (Exception ex)
1220 | {
1221 | Log($"Http Error: {ex.Message}", LogLevel.Error, nameof(DownloadFileByUrl));
1222 | if (i < http_retries) Log($"Retry({i + 1}): {file_info.durl}", LogLevel.Info, nameof(DownloadFileByUrl));
1223 | }
1224 | }
1225 |
1226 | });
1227 | }
1228 |
1229 | if (!isDownloadSuccess && isCanceled)
1230 | {
1231 | result = new DownloadInfo(LanZouCode.TASK_CANCELED, _task_canceled_msg);
1232 | }
1233 | else if (!isDownloadSuccess && !isCanceled)
1234 | {
1235 | result = new DownloadInfo(LanZouCode.NETWORK_ERROR, "Download failed, retry failed.");
1236 | }
1237 | else
1238 | {
1239 | if (overwrite && File.Exists(file_path))
1240 | {
1241 | File.Delete(file_path);
1242 | Log($"Overwrire delete file path: {file_path}", LogLevel.Info, nameof(DownloadFileByUrl));
1243 | }
1244 |
1245 | Log($"Move tmp file to real path: {file_path}", LogLevel.Info, nameof(DownloadFileByUrl));
1246 | // 下载完成,改回正常文件名
1247 | File.Move(tmp_file_path, file_path);
1248 |
1249 | var p_finish = new ProgressInfo(ProgressState.Finish, filename, now_size, content_length);
1250 | progress?.Report(p_finish);
1251 |
1252 | await Task.Yield(); // 保证 progress report 到达
1253 | await Task.Yield(); // 保证 progress report 到达
1254 |
1255 | result = new DownloadInfo(LanZouCode.SUCCESS, _success_msg, share_url, filename, file_path, is_continue);
1256 | }
1257 |
1258 | LogResult(result, nameof(DownloadFileByUrl));
1259 | return result;
1260 | }
1261 |
1262 | ///
1263 | /// 通过文件分享链接,获取文件各种信息(包括下载直链,需提取码)
1264 | /// 此接口无需登录
1265 | ///
1266 | /// 文件分享链接
1267 | /// 文件提取码(如果有的话)
1268 | ///
1269 | public async Task GetFileInfoByUrl(string share_url, string pwd = null)
1270 | {
1271 | LogInfo($"Get file info of url: {share_url}", nameof(GetFileInfoByUrl));
1272 |
1273 | CloudFileInfo result = null;
1274 | string first_page = null;
1275 |
1276 | if (!await is_share_url(share_url))
1277 | {
1278 | result = new CloudFileInfo(LanZouCode.URL_INVALID, $"Invalid url: {share_url}", pwd, share_url);
1279 | }
1280 | else if (string.IsNullOrEmpty(first_page = await _get_text(share_url))) // 文件分享页面(第一页)
1281 | {
1282 | result = new CloudFileInfo(LanZouCode.NETWORK_ERROR, _network_error_msg, pwd, share_url);
1283 | }
1284 | else if (first_page.Contains("acw_sc__v2"))
1285 | {
1286 | // 在页面被过多访问或其他情况下,有时候会先返回一个加密的页面,其执行计算出一个acw_sc__v2后放入页面后再重新访问页面才能获得正常页面
1287 | // 若该页面进行了js加密,则进行解密,计算acw_sc__v2,并加入cookie
1288 | var acw_sc__v2 = calc_acw_sc__v2(first_page);
1289 | _set_cookie(new Uri(share_url).Host, "acw_sc__v2", $"{acw_sc__v2}");
1290 | Log($"Set Cookie: acw_sc__v2={acw_sc__v2}", LogLevel.Info, nameof(GetFileInfoByUrl));
1291 | first_page = await _get_text(share_url); // 文件分享页面(第一页)
1292 | if (string.IsNullOrEmpty(first_page))
1293 | {
1294 | result = new CloudFileInfo(LanZouCode.NETWORK_ERROR, _network_error_msg, pwd, share_url);
1295 | }
1296 | }
1297 |
1298 | if (result != null)
1299 | {
1300 | LogResult(result, nameof(GetFileInfoByUrl));
1301 | return result;
1302 | }
1303 |
1304 | first_page = remove_notes(first_page); // 去除网页里的注释
1305 | if (first_page.Contains("文件取消") || first_page.Contains("文件不存在"))
1306 | {
1307 | result = new CloudFileInfo(LanZouCode.FILE_CANCELLED, $"文件取消分享或不存在: {share_url}", pwd, share_url);
1308 | LogResult(result, nameof(GetFileInfoByUrl));
1309 | return result;
1310 | }
1311 |
1312 | JsonData link_info;
1313 | string f_name;
1314 | string f_time;
1315 | string f_size;
1316 | string f_desc;
1317 | string f_type;
1318 |
1319 | // 这里获取下载直链 304 重定向前的链接
1320 | // 文件设置了提取码时
1321 | if (first_page.Contains("id=\"pwdload\"") || first_page.Contains("id=\"passwddiv\""))
1322 | {
1323 | if (string.IsNullOrEmpty(pwd))
1324 | {
1325 | // 没给提取码直接退出
1326 | result = new CloudFileInfo(LanZouCode.LACK_PASSWORD, $"分享链接需要提取码: {share_url}", pwd, share_url);
1327 | LogResult(result, nameof(GetFileInfoByUrl));
1328 | return result;
1329 | }
1330 |
1331 | var sign = Regex.Match(first_page, "sign=(\\w+?)&").Groups[1].Value;
1332 | var post_data = _post_data("action", "downprocess", "sign", $"{sign}", "p", $"{pwd}");
1333 | var link_info_str = await _post_text(_host_url + "/ajaxm.php", post_data); // 保存了重定向前的链接信息和文件名
1334 | var second_page = await _get_text(share_url); // 再次请求文件分享页面,可以看见文件名,时间,大小等信息(第二页)
1335 | if (string.IsNullOrEmpty(link_info_str) || string.IsNullOrEmpty(second_page))
1336 | {
1337 | result = new CloudFileInfo(LanZouCode.NETWORK_ERROR, _network_error_msg, pwd, share_url);
1338 | LogResult(result, nameof(GetFileInfoByUrl));
1339 | return result;
1340 | }
1341 |
1342 | link_info = JsonMapper.ToObject(link_info_str);
1343 | second_page = remove_notes(second_page);
1344 |
1345 | // 提取文件信息
1346 | f_name = link_info["inf"].ToString().Replace("*", "_");
1347 | f_type = Path.GetExtension(f_name).Substring(1);
1348 |
1349 | var f_size_match = Regex.Match(second_page, "大小.+?(\\d[\\d.,]+\\s?[BKM]?)<");
1350 | f_size = f_size_match.Success ? f_size_match.Groups[1].Value.Replace(",", "") : "0 M";
1351 |
1352 | var f_time_match = Regex.Match(second_page, "class=\"n_file_infos\">(.+?)");
1353 | f_time = f_time_match.Success ? time_format(f_time_match.Groups[1].Value) : time_format("0 小时前");
1354 |
1355 | var f_desc_match = Regex.Match(second_page, "class=\"n_box_des\">(.*?)");
1356 | f_desc = f_desc_match.Success ? f_desc_match.Groups[1].Value : "";
1357 | }
1358 | else // 文件没有设置提取码时,文件信息都暴露在分享页面上
1359 | {
1360 | var para = Regex.Match(first_page, "(.+?) - 蓝奏云")).Success
1365 | || (f_name_match = Regex.Match(first_page, "([^<>]+?)
")).Success
1366 | || (f_name_match = Regex.Match(first_page, "([^<>].+?)
")).Success
1367 | || (f_name_match = Regex.Match(first_page, "var filename = '(.+?)';")).Success
1368 | || (f_name_match = Regex.Match(first_page, "id=\"filenajax\">(.+?)")).Success
1369 | || (f_name_match = Regex.Match(first_page, "([^<>]+?)
")).Success)
1370 | f_name = f_name_match.Groups[1].Value.Replace("*", "_");
1371 | else
1372 | f_name = "未匹配到文件名";
1373 |
1374 | f_type = Path.GetExtension(f_name).Substring(1);
1375 |
1376 | // 匹配文件时间,文件没有时间信息就视为今天,统一表示为 2020-01-01 格式
1377 | var f_time_match = Regex.Match(first_page, @">(\d+\s?[秒天分小][钟时]?前|[昨前]天\s?[\d:]+?|\d+\s?天前|\d{4}-\d\d-\d\d)<");
1378 | f_time = f_time_match.Success ? time_format(f_time_match.Groups[1].Value) : time_format("0 小时前");
1379 |
1380 | // 匹配文件大小
1381 | var f_size_match = Regex.Match(first_page, @"大小.+?(\d[\d.,]+\s?[BKM]?)<");
1382 | f_size = f_size_match.Success ? f_size_match.Groups[1].Value.Replace(",", "") : "0 M";
1383 |
1384 | // 匹配文件描述
1385 | var f_desc_match = Regex.Match(first_page, @"文件描述.+?
\n?\s*(.*?)\s*");
1386 | f_desc = f_desc_match.Success ? f_desc_match.Groups[1].Value : "";
1387 |
1388 | first_page = await _get_text(_host_url + para);
1389 | if (string.IsNullOrEmpty(first_page))
1390 | {
1391 | result = new CloudFileInfo(LanZouCode.NETWORK_ERROR, _network_error_msg, pwd, share_url, f_name, f_type, f_time, f_size, f_desc);
1392 | LogResult(result, nameof(GetFileInfoByUrl));
1393 | return result;
1394 | }
1395 |
1396 | first_page = remove_notes(first_page);
1397 | // 一般情况 sign 的值就在 data 里,有时放在变量后面
1398 | var sign = Regex.Match(first_page, "'sign':(.+?),").Groups[1].Value;
1399 | if (sign.Length < 20) // 此时 sign 保存在变量里面, 变量名是 sign 匹配的字符
1400 | sign = Regex.Match(first_page, $"var {sign}\\s*=\\s*'(.+?)';").Groups[1].Value;
1401 |
1402 | var post_data = _post_data("action", "downprocess", "sign", $"{sign}", "ves", $"{1}");
1403 | var link_info_str = await _post_text(_host_url + "/ajaxm.php", post_data);
1404 | if (string.IsNullOrEmpty(link_info_str))
1405 | {
1406 | result = new CloudFileInfo(LanZouCode.NETWORK_ERROR, _network_error_msg, pwd, share_url, f_name, f_type, f_time, f_size, f_desc);
1407 | LogResult(result, nameof(GetFileInfoByUrl));
1408 | return result;
1409 | }
1410 |
1411 | link_info = JsonMapper.ToObject(link_info_str);
1412 | }
1413 |
1414 | // 这里开始获取文件直链
1415 | if ((int)link_info["zt"] != 1) //# 返回信息异常,无法获取直链
1416 | {
1417 | result = new CloudFileInfo(LanZouCode.FAILED, "无法获取直链", pwd, share_url, f_name, f_type, f_time, f_size, f_desc);
1418 | LogResult(result, nameof(GetFileInfoByUrl));
1419 | return result;
1420 | }
1421 |
1422 | var fake_url = link_info["dom"].ToString() + "/file/" + link_info["url"].ToString(); // 假直连,存在流量异常检测
1423 | string download_page_html = null;
1424 | string redirect_url = null;
1425 |
1426 | for (int i = 0; i < http_retries; i++)
1427 | {
1428 | try
1429 | {
1430 | using (var client = _get_client(null, 0, false))
1431 | {
1432 | using (var resp = await client.GetAsync(fake_url))
1433 | {
1434 | if (resp.StatusCode == HttpStatusCode.OK)
1435 | {
1436 | // 假直连,需要重新获取
1437 | }
1438 | else if (resp.StatusCode == HttpStatusCode.Found)
1439 | {
1440 | redirect_url = resp.Headers.Location.AbsoluteUri;// 重定向后的真直链
1441 | }
1442 | else // 未知网络错误
1443 | {
1444 | result = new CloudFileInfo(LanZouCode.NETWORK_ERROR, _network_error_msg, pwd, share_url, f_name, f_type, f_time, f_size, f_desc);
1445 | LogResult(result, nameof(GetFileInfoByUrl));
1446 | return result;
1447 | }
1448 |
1449 | // download_page.encoding = 'utf-8'
1450 | download_page_html = await resp.Content.ReadAsStringAsync();
1451 | download_page_html = remove_notes(download_page_html);
1452 | }
1453 | }
1454 | break;
1455 | }
1456 | catch (Exception ex)
1457 | {
1458 | Log($"Http Error: {ex.Message}", LogLevel.Error, nameof(GetFileInfoByUrl));
1459 | if (i < http_retries) Log($"Retry({i + 1}): {fake_url}", LogLevel.Info, nameof(GetFileInfoByUrl));
1460 | }
1461 | }
1462 |
1463 | string direct_url;
1464 | if (!download_page_html.Contains("网络异常")) // 没有遇到验证码
1465 | {
1466 | direct_url = redirect_url;
1467 | }
1468 | else // 遇到验证码,验证后才能获取下载直链
1469 | {
1470 | Log($"Get direct url need verify code, force wait for 2 seconds.", LogLevel.Warning, nameof(GetFileInfoByUrl));
1471 | var file_token = Regex.Match(download_page_html, "'file':'(.+?)'").Groups[1].Value;
1472 | var file_sign = Regex.Match(download_page_html, "'sign':'(.+?)'").Groups[1].Value;
1473 | var check_api = "https://vip.d0.baidupan.com/file/ajax.php";
1474 | var post_data = _post_data("file", $"{file_token}", "el", $"{2}", "sign", $"{file_sign}");
1475 | await Task.Delay(2000); // 这里必需等待2s, 否则直链返回 ?SignError
1476 | var text = await _post_text(check_api, post_data);
1477 | var json = JsonMapper.ToObject(text);
1478 | direct_url = json["url"].ToString();
1479 | if (string.IsNullOrEmpty(direct_url) || direct_url.Contains("SignError"))
1480 | {
1481 | result = new CloudFileInfo(LanZouCode.CAPTCHA_ERROR, "验证失败", pwd, share_url, f_name, f_type, f_time, f_size, f_desc);
1482 | LogResult(result, nameof(GetFileInfoByUrl));
1483 | return result;
1484 | }
1485 | }
1486 |
1487 | result = new CloudFileInfo(LanZouCode.SUCCESS, _success_msg, pwd, share_url, f_name, f_type, f_time, f_size, f_desc, direct_url);
1488 | LogResult(result, nameof(GetFileInfoByUrl));
1489 | return result;
1490 | }
1491 |
1492 | ///
1493 | /// 通过分享链接,获取文件夹及其子文件信息(需提取码)
1494 | /// 官方默认每页 50 条数据
1495 | /// 此接口无需登录
1496 | ///
1497 | /// 分享链接
1498 | /// 提取码
1499 | /// 开始页码,默认值 1 为起始页
1500 | /// 获取页数,默认值 -1 表示所有
1501 | ///
1502 | public async Task GetFolderInfoByUrl(string share_url, string pwd = null, int page_begin = 1, int page_count = -1)
1503 | {
1504 | LogInfo($"Get folder info of url: {share_url}, begin page : {page_begin}, count: {page_count}", nameof(GetFolderInfoByUrl));
1505 |
1506 | CloudFolderInfo result = null;
1507 |
1508 | // TODO: Invalid url
1509 | if (await is_share_url(share_url))
1510 | {
1511 | result = new CloudFolderInfo(LanZouCode.URL_INVALID, $"Invalid url: {share_url}");
1512 | LogResult(result, nameof(GetFolderInfoByUrl));
1513 | return result;
1514 | }
1515 |
1516 | var html = await _get_text(share_url);
1517 | if (string.IsNullOrEmpty(html))
1518 | {
1519 | result = new CloudFolderInfo(LanZouCode.NETWORK_ERROR, _network_error_msg);
1520 | }
1521 | else if (html.Contains("文件不存在") || html.Contains("文件取消"))
1522 | {
1523 | result = new CloudFolderInfo(LanZouCode.FILE_CANCELLED, "文件取消分享或不存在");
1524 | }
1525 | // 要求输入密码, 用户描述中可能带有"输入密码",所以不用这个字符串判断
1526 | else if (string.IsNullOrEmpty(pwd) && (html.Contains("id=\"pwdload\"") || html.Contains("id=\"passwddiv\"")))
1527 | {
1528 | result = new CloudFolderInfo(LanZouCode.LACK_PASSWORD, $"分享链接需要提取码: {share_url}");
1529 | }
1530 | else if (html.Contains("acw_sc__v2"))
1531 | {
1532 | // 在页面被过多访问或其他情况下,有时候会先返回一个加密的页面,其执行计算出一个acw_sc__v2后放入页面后再重新访问页面才能获得正常页面
1533 | // 若该页面进行了js加密,则进行解密,计算acw_sc__v2,并加入cookie
1534 | var acw_sc__v2 = calc_acw_sc__v2(html);
1535 | _set_cookie(new Uri(share_url).Host, "acw_sc__v2", acw_sc__v2);
1536 | Log($"Set Cookie: acw_sc__v2={acw_sc__v2}", LogLevel.Info, nameof(GetFolderInfoByUrl));
1537 | html = await _get_text(share_url); // 文件分享页面(第一页)
1538 | if (string.IsNullOrEmpty(html))
1539 | {
1540 | result = new CloudFolderInfo(LanZouCode.NETWORK_ERROR, _network_error_msg);
1541 | }
1542 | }
1543 |
1544 | // 以上粗略校验失败,直接返回错误
1545 | if (result != null)
1546 | {
1547 | LogResult(result, nameof(GetFolderInfoByUrl));
1548 | return result;
1549 | }
1550 |
1551 | // 获取文件需要的参数
1552 | html = remove_notes(html);
1553 |
1554 | var re_lx = Regex.Match(html, "'lx':'?(\\d)'?,");
1555 | if (!re_lx.Success)
1556 | {
1557 | result = new CloudFolderInfo(LanZouCode.FAILED, $"Regex Failed: lx");
1558 | LogResult(result, nameof(GetFolderInfoByUrl));
1559 | return result;
1560 | }
1561 |
1562 | var lx = re_lx.Groups[1].Value;
1563 |
1564 | var re_t = Regex.Match(html, "var [0-9a-z]{6} = '(\\d{10})';");
1565 | if (!re_t.Success)
1566 | {
1567 | result = new CloudFolderInfo(LanZouCode.FAILED, $"Regex Failed: t");
1568 | LogResult(result, nameof(GetFolderInfoByUrl));
1569 | return result;
1570 | }
1571 |
1572 | var t = re_t.Groups[1].Value;
1573 |
1574 | var re_k = Regex.Match(html, "var [0-9a-z]{6} = '([0-9a-z]{15,})';");
1575 | if (!re_k.Success)
1576 | {
1577 | result = new CloudFolderInfo(LanZouCode.FAILED, $"Regex Failed: k");
1578 | LogResult(result, nameof(GetFolderInfoByUrl));
1579 | return result;
1580 | }
1581 |
1582 | var k = re_k.Groups[1].Value;
1583 |
1584 | // 文件夹的信息
1585 | var re_folder_id = Regex.Match(html, "'fid':'?(\\d+)'?,");
1586 | if (!re_folder_id.Success)
1587 | {
1588 | result = new CloudFolderInfo(LanZouCode.FAILED, $"Regex Failed: folder_id");
1589 | LogResult(result, nameof(GetFolderInfoByUrl));
1590 | return result;
1591 | }
1592 |
1593 | var folder_id = long.Parse(re_folder_id.Groups[1].Value);
1594 |
1595 | var re_folder_name = Regex.Match(html, "var.+?='(.+?)';\n.+document.title");
1596 | if (!re_folder_name.Success) re_folder_name = Regex.Match(html, "(.+?)
");
1597 | if (!re_folder_name.Success)
1598 | {
1599 | result = new CloudFolderInfo(LanZouCode.FAILED, $"Regex Failed: folder_name");
1600 | LogResult(result, nameof(GetFolderInfoByUrl));
1601 | return result;
1602 | }
1603 |
1604 | var folder_name = re_folder_name.Groups[1].Value;
1605 |
1606 | var folder_time = "";
1607 | var re_folder_time = Regex.Match(html, "class=\"rets\">([\\d\\-]+?)(.+?)");
1612 | if (!re_folder_desc.Success) re_folder_desc = Regex.Match(html, "(.+?)");
1613 | if (re_folder_desc.Success) folder_desc = re_folder_desc.Groups[1].Value;
1614 |
1615 |
1616 | // 提取子文件夹信息(vip用户分享的文件夹可以递归包含子文件夹)
1617 | var sub_folders = new List();
1618 | // 文件夹描述放在 filesize 一栏, 迷惑行为
1619 | var re_all_sub_folders = Regex.Matches(html, "mbxfolder\">(.+?)(.*?)
");
1620 | for (int i = 0; i < re_all_sub_folders.Count; i++)
1621 | {
1622 | var m = re_all_sub_folders[i];
1623 | var url = _host_url + m.Groups[0].Value;
1624 | var name = m.Groups[1].Value;
1625 | var desc = m.Groups[2].Value;
1626 | var folder = new SubFolder() { name = name, description = desc, url = url };
1627 | sub_folders.Add(folder);
1628 | }
1629 |
1630 | Log($"Get folder info, sub folders: {sub_folders.Count}", LogLevel.Info, nameof(GetFolderInfoByUrl));
1631 |
1632 | // 提取文件夹下全部文件
1633 | long page = page_begin;
1634 | long page_end = page_count < 0 ? long.MaxValue : ((long)page_begin + page_count);
1635 | var sub_files = new List();
1636 | while (page < page_end)
1637 | {
1638 | if (page > page_begin) // 连续的请求需要稍等一下
1639 | await Task.Delay(800);
1640 |
1641 | Log($"Get folder info page {page}...", LogLevel.Info, nameof(GetFolderInfoByUrl));
1642 |
1643 | var post_data = _post_data("lx", lx, "pg", $"{page}", "k", k, "t", t, "fid", $"{folder_id}", "pwd", pwd);
1644 | var text = await _post_text(_host_url + "/filemoreajax.php", post_data);
1645 | if (string.IsNullOrEmpty(text))
1646 | {
1647 | result = new CloudFolderInfo(LanZouCode.NETWORK_ERROR, _network_error_msg);
1648 | LogResult(result, nameof(GetFolderInfoByUrl));
1649 | return result;
1650 | }
1651 |
1652 | var json = JsonMapper.ToObject(text);
1653 |
1654 | var zt = int.Parse(json["zt"].ToString());
1655 | if (zt == 1) // 成功获取一页文件信息
1656 | {
1657 | foreach (var j_f in json["text"])
1658 | {
1659 | var _json = (JsonData)j_f;
1660 | var _name = _json["name_all"].ToString(); // 文件名
1661 | var _time = _json["time"].ToString(); // 上传时间
1662 | var _size = _json["size"].ToString(); // 文件大小
1663 | var _type = Path.GetExtension(_json["name_all"].ToString()).Substring(1); // 文件格式
1664 | var _url = _host_url + "/" + _json["id"].ToString(); // 文件分享链接
1665 | var _sub_file = new SubFile() { name = _name, time = _time, size = _size, type = _type, url = _url };
1666 | sub_files.Add(_sub_file);
1667 | };
1668 | page += 1; // 下一页
1669 | continue;
1670 | }
1671 | else if (zt == 2) // 已经拿到全部的文件信息
1672 | {
1673 | break;
1674 | }
1675 | else if (zt == 3) // 提取码错误
1676 | {
1677 | result = new CloudFolderInfo(LanZouCode.PASSWORD_ERROR, $"Password error: {pwd}");
1678 | LogResult(result, nameof(GetFolderInfoByUrl));
1679 | return result;
1680 | }
1681 | else if (zt == 4) // 发送频繁等原因,需要重试
1682 | {
1683 | Log($"Get folder info page {page} failed({json["info"]})", LogLevel.Warning, nameof(GetFolderInfoByUrl));
1684 | continue;
1685 | }
1686 | else // 其它未知错误
1687 | {
1688 | result = new CloudFolderInfo(LanZouCode.FAILED, $"Unkown error zt:{zt}");
1689 | LogResult(result, nameof(GetFolderInfoByUrl));
1690 | return result;
1691 | }
1692 | }
1693 |
1694 | Log($"Get folder info, sub files: {sub_files.Count}", LogLevel.Info, nameof(GetFolderInfoByUrl));
1695 |
1696 | result = new CloudFolderInfo(LanZouCode.SUCCESS, _success_msg, folder_id, folder_name,
1697 | folder_time, pwd, folder_desc, share_url, sub_folders, sub_files);
1698 | LogResult(result, nameof(GetFolderInfoByUrl));
1699 | return result;
1700 | }
1701 | #endregion
1702 |
1703 | }
1704 | }
1705 |
--------------------------------------------------------------------------------
/src/LanZouCloud/Code/LanZouCode.cs:
--------------------------------------------------------------------------------
1 | namespace LanZouCloudAPI
2 | {
3 | public enum LanZouCode
4 | {
5 | FAILED = -1,
6 | SUCCESS = 0,
7 | PASSWORD_ERROR = 2,
8 | LACK_PASSWORD = 3,
9 | URL_INVALID = 6,
10 | FILE_CANCELLED = 7,
11 | PATH_ERROR = 8,
12 | NETWORK_ERROR = 9,
13 | CAPTCHA_ERROR = 10,
14 | OFFICIAL_LIMITED = 11,
15 | NOT_LOGIN = 12,
16 | TASK_CANCELED = 13,
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/LanZouCloud/Code/LanZouModels.cs:
--------------------------------------------------------------------------------
1 | using LitJson;
2 | using System.Collections.Generic;
3 |
4 | namespace LanZouCloudAPI
5 | {
6 | ///
7 | /// 重写ToString,以JSON格式输出
8 | ///
9 | public class JsonStringObject
10 | {
11 | public override string ToString()
12 | {
13 | return JsonMapper.ToJson(this);
14 | }
15 | }
16 |
17 | ///
18 | /// 蓝奏云返回结果信息
19 | ///
20 | public class Result : JsonStringObject
21 | {
22 | ///
23 | /// 蓝奏云结果码
24 | ///
25 | public LanZouCode code { get; internal set; }
26 |
27 | ///
28 | /// 错误消息 或 成功消息
29 | ///
30 | public string message { get; internal set; }
31 |
32 | internal Result() { }
33 |
34 | public Result(LanZouCode code, string errorMessage)
35 | {
36 | this.code = code;
37 | this.message = errorMessage;
38 | }
39 | }
40 |
41 | internal class MoveFolderList : Result
42 | {
43 | internal Dictionary folders { get; set; }
44 |
45 | internal MoveFolderList(LanZouCode code, string errorMessage, Dictionary folders)
46 | {
47 | this.code = code;
48 | this.message = errorMessage;
49 | this.folders = folders;
50 | }
51 | }
52 |
53 | public class CloudFile : JsonStringObject
54 | {
55 | ///
56 | /// 文件唯一ID
57 | ///
58 | public long id { get; internal set; }
59 |
60 | ///
61 | /// 文件名
62 | ///
63 | public string name { get; internal set; }
64 |
65 | ///
66 | /// 上传时间
67 | ///
68 | public string time { get; internal set; }
69 |
70 | ///
71 | /// 文件大小
72 | ///
73 | public string size { get; internal set; }
74 |
75 | ///
76 | /// 文件类型
77 | ///
78 | public string type { get; internal set; }
79 |
80 | ///
81 | /// 下载次数
82 | ///
83 | public int downloads { get; internal set; }
84 |
85 | ///
86 | /// 是否存在提取码
87 | ///
88 | public bool hasPassword { get; internal set; }
89 |
90 | ///
91 | /// 是否有描述信息
92 | ///
93 | public bool hasDescription { get; internal set; }
94 | }
95 |
96 | public class CloudFolder : JsonStringObject
97 | {
98 | ///
99 | /// 文件夹唯一ID
100 | ///
101 | public long id { get; internal set; }
102 |
103 | ///
104 | /// 文件夹名
105 | ///
106 | public string name { get; internal set; }
107 |
108 | ///
109 | /// 是否存在提取码
110 | ///
111 | public bool hasPassword { get; internal set; }
112 |
113 | ///
114 | /// 文件夹描述信息
115 | ///
116 | public string description { get; internal set; }
117 | }
118 |
119 | public class CloudFileList : Result
120 | {
121 | ///
122 | /// 文件列表
123 | ///
124 | public List files { get; internal set; }
125 |
126 | internal CloudFileList(LanZouCode code, string errorMessage, List files = null)
127 | {
128 | this.code = code;
129 | this.message = errorMessage;
130 | this.files = files;
131 | }
132 | }
133 |
134 | public class CloudFolderList : Result
135 | {
136 | ///
137 | /// 文件夹列表
138 | ///
139 | public List folders { get; internal set; }
140 |
141 | internal CloudFolderList(LanZouCode code, string errorMessage, List folders = null)
142 | {
143 | this.code = code;
144 | this.message = errorMessage;
145 | this.folders = folders;
146 | }
147 | }
148 |
149 | ///
150 | /// 文件分享页信息
151 | ///
152 | public class CloudFileInfo : Result
153 | {
154 | ///
155 | /// 文件名称
156 | ///
157 | public string name { get; internal set; }
158 |
159 | ///
160 | /// 提取码
161 | ///
162 | public string password { get; internal set; }
163 |
164 | ///
165 | /// 描述
166 | ///
167 | public string description { get; internal set; }
168 |
169 | ///
170 | /// 分享链接
171 | ///
172 | public string url { get; internal set; }
173 |
174 | ///
175 | /// 文件大小
176 | ///
177 | public string size { get; internal set; }
178 |
179 | ///
180 | /// 上传时间
181 | ///
182 | public string time { get; internal set; }
183 |
184 | ///
185 | /// 文件类型
186 | ///
187 | public string type { get; internal set; }
188 |
189 | ///
190 | /// 直连地址(下载地址)
191 | ///
192 | public string durl { get; internal set; }
193 |
194 | internal CloudFileInfo(LanZouCode code, string errorMessage, string password = null, string url = null,
195 | string name = null, string type = null, string time = null, string size = null,
196 | string description = null, string durl = null)
197 | {
198 | this.code = code;
199 | this.message = errorMessage;
200 | this.password = password;
201 | this.url = url;
202 | this.name = name;
203 | this.type = type;
204 | this.time = time;
205 | this.size = size;
206 | this.description = description;
207 | this.durl = durl;
208 | }
209 | }
210 |
211 |
212 | ///
213 | /// 专为 CloudFolderInfo 使用,指其下子文件夹
214 | ///
215 | public class SubFolder : JsonStringObject
216 | {
217 | ///
218 | /// 文件夹名
219 | ///
220 | public string name { get; internal set; }
221 |
222 | ///
223 | /// 描述
224 | ///
225 | public string description { get; internal set; }
226 |
227 | ///
228 | /// 分享链接
229 | ///
230 | public string url { get; internal set; }
231 | }
232 |
233 | ///
234 | /// 专为 CloudFolderInfo 使用,指其下子文件
235 | ///
236 | public class SubFile : JsonStringObject
237 | {
238 | ///
239 | /// 文件名
240 | ///
241 | public string name { get; internal set; }
242 |
243 | ///
244 | /// 上传时间
245 | ///
246 | public string time { get; internal set; }
247 |
248 | ///
249 | /// 文件大小
250 | ///
251 | public string size { get; internal set; }
252 |
253 | ///
254 | /// 文件类型
255 | ///
256 | public string type { get; internal set; }
257 |
258 | ///
259 | /// 分享链接
260 | ///
261 | public string url { get; internal set; }
262 | }
263 |
264 | ///
265 | /// 文件夹分享页信息,包括子文件(夹)信息
266 | ///
267 | public class CloudFolderInfo : Result
268 | {
269 | ///
270 | /// 文件夹唯一ID
271 | ///
272 | public long id { get; internal set; }
273 |
274 | ///
275 | /// 文件夹名
276 | ///
277 | public string name { get; internal set; }
278 |
279 | ///
280 | /// 创建时间
281 | ///
282 | public string time { get; internal set; }
283 |
284 | ///
285 | /// 提取码
286 | ///
287 | public string password { get; internal set; }
288 |
289 | ///
290 | /// 描述
291 | ///
292 | public string description { get; internal set; }
293 |
294 | ///
295 | /// 分享链接
296 | ///
297 | public string url { get; internal set; }
298 |
299 | ///
300 | /// 子文件夹列表
301 | ///
302 | public List folders { get; internal set; }
303 |
304 | ///
305 | /// 子文件列表
306 | ///
307 | public List files { get; internal set; }
308 |
309 | internal CloudFolderInfo(LanZouCode code, string errorMessage, long id = 0,
310 | string name = null, string time = null, string password = null,
311 | string description = null, string url = null,
312 | List folders = null, List files = null)
313 | {
314 | this.code = code;
315 | this.message = errorMessage;
316 | this.id = id;
317 | this.name = name;
318 | this.time = time;
319 | this.description = description;
320 | this.password = password;
321 | this.url = url;
322 | this.folders = folders;
323 | this.files = files;
324 | }
325 | }
326 |
327 | ///
328 | /// 分享文件(夹)信息
329 | ///
330 | public class ShareInfo : Result
331 | {
332 | ///
333 | /// 文件(夹)名
334 | ///
335 | public string name { get; internal set; }
336 |
337 | ///
338 | /// 描述
339 | ///
340 | public string description { get; internal set; }
341 |
342 | ///
343 | /// 分享链接
344 | ///
345 | public string url { get; internal set; }
346 |
347 | ///
348 | /// 提取码
349 | ///
350 | public string password { get; internal set; }
351 |
352 | internal ShareInfo(LanZouCode code, string errorMessage,
353 | string name = null, string url = null, string description = null,
354 | string password = null)
355 | {
356 | this.code = code;
357 | this.message = errorMessage;
358 | this.description = description;
359 | this.name = name;
360 | this.password = password;
361 | this.url = url;
362 | }
363 | }
364 |
365 | ///
366 | /// 创建文件夹返回结果
367 | ///
368 | public class CreateFolderInfo : Result
369 | {
370 | ///
371 | /// 文件夹唯一ID
372 | ///
373 | public long id { get; internal set; }
374 |
375 | ///
376 | /// 文件夹名
377 | ///
378 | public string name { get; internal set; }
379 |
380 | ///
381 | /// 描述
382 | ///
383 | public string description { get; internal set; }
384 |
385 | internal CreateFolderInfo(LanZouCode code, string errorMessage,
386 | long id = 0, string name = null, string description = null)
387 | {
388 | this.code = code;
389 | this.message = errorMessage;
390 | this.id = id;
391 | this.name = name;
392 | this.description = description;
393 | }
394 | }
395 |
396 | ///
397 | /// 移动文件夹返回结果
398 | ///
399 | public class MoveFolderInfo : Result
400 | {
401 | ///
402 | /// 文件夹唯一ID
403 | ///
404 | public long id { get; internal set; }
405 |
406 | ///
407 | /// 文件夹名
408 | ///
409 | public string name { get; internal set; }
410 |
411 | ///
412 | /// 描述
413 | ///
414 | public string description { get; internal set; }
415 |
416 | internal MoveFolderInfo(LanZouCode code, string errorMessage,
417 | long id = 0, string name = null, string description = null)
418 | {
419 | this.code = code;
420 | this.message = errorMessage;
421 | this.id = id;
422 | this.name = name;
423 | this.description = description;
424 | }
425 | }
426 |
427 | public class DownloadInfo : Result
428 | {
429 | ///
430 | /// 文件名
431 | ///
432 | public string fileName { get; internal set; }
433 |
434 | ///
435 | /// 下载路径
436 | ///
437 | public string filePath { get; internal set; }
438 |
439 | ///
440 | /// 分享链接
441 | ///
442 | public string url { get; internal set; }
443 |
444 | ///
445 | /// 直连地址(下载地址)
446 | ///
447 | public string durl { get; internal set; }
448 |
449 | ///
450 | /// 是否断点续传
451 | ///
452 | public bool isContinue { get; internal set; }
453 |
454 | internal DownloadInfo(LanZouCode code, string errorMessage, string url = null,
455 | string fileName = null, string filePath = null, bool isContinue = false)
456 | {
457 | this.code = code;
458 | this.message = errorMessage;
459 | this.url = url;
460 | this.fileName = fileName;
461 | this.filePath = filePath;
462 | this.isContinue = isContinue;
463 | }
464 | }
465 |
466 | public class UploadInfo : Result
467 | {
468 | ///
469 | /// 文件名
470 | ///
471 | public string fileName { get; internal set; }
472 |
473 | ///
474 | /// 本地文件路径
475 | ///
476 | public string filePath { get; internal set; }
477 |
478 | ///
479 | /// 文件唯一ID
480 | ///
481 | public long id { get; internal set; }
482 |
483 | ///
484 | /// 分享链接
485 | ///
486 | public string url { get; internal set; }
487 |
488 | internal UploadInfo(LanZouCode code, string errorMessage, string fileName = null,
489 | string filePath = null, long id = 0, string url = null)
490 | {
491 | this.code = code;
492 | this.message = errorMessage;
493 | this.fileName = fileName;
494 | this.filePath = filePath;
495 | this.id = id;
496 | this.url = url;
497 | }
498 | }
499 |
500 | public enum ProgressState
501 | {
502 | Start,
503 | Ready,
504 | Progressing,
505 | Finish,
506 | }
507 |
508 | ///
509 | /// 上传/下载 进度信息
510 | ///
511 | public class ProgressInfo : JsonStringObject
512 | {
513 | ///
514 | /// 状态
515 | ///
516 | public ProgressState state { get; internal set; }
517 |
518 | ///
519 | /// 文件名
520 | ///
521 | public string fileName { get; internal set; }
522 |
523 | ///
524 | /// 当前大小(字节)
525 | ///
526 | public long current { get; internal set; }
527 |
528 | ///
529 | /// 总大小(字节)
530 | ///
531 | public long total { get; internal set; }
532 |
533 | internal ProgressInfo(ProgressState state, string filename = null, long current = 0, long total = 0)
534 | {
535 | this.state = state;
536 | this.fileName = filename;
537 | this.current = current;
538 | this.total = total;
539 | }
540 | }
541 |
542 | }
543 |
--------------------------------------------------------------------------------
/src/LanZouCloud/LanZouCloud.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/LanZouCloud.Tests/AConfig.cs:
--------------------------------------------------------------------------------
1 | using LanZouCloudAPI;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace Test
11 | {
12 | public static class AConfig
13 | {
14 | public const string TestFolder = "LanZouApiTestFolder";
15 |
16 | public const string RootPath = "../../../../../../LanZouTest/";
17 | public const string TestFilePath = RootPath + "LanZouApiTestFile.txt";
18 | public const string TestFileBigPath = RootPath + "LanZouApiTestFileBig.w3x";
19 | public const string cookieFilePath = RootPath + "cookie.txt";
20 |
21 |
22 | public static LanZouCloud Cloud()
23 | {
24 | var cloud = new LanZouCloud();
25 | cloud.SetLogLevel(LanZouCloud.LogLevel.Info);
26 | return cloud;
27 | }
28 |
29 | public static async Task AsyncLoginCloud(bool validate = false)
30 | {
31 | var cloud = new LanZouCloud();
32 | cloud.SetLogLevel(LanZouCloud.LogLevel.Info);
33 | string[] cookies = File.ReadAllText(cookieFilePath).Split(',');
34 | var login = await cloud.Login(cookies[0], cookies[1], validate);
35 | Assert.IsTrue(login.code == LanZouCode.SUCCESS);
36 | return cloud;
37 | }
38 |
39 | public static async Task GetTestFolder(LanZouCloud cloud, string name = null, long parent_id = -1)
40 | {
41 | name = name ?? TestFolder;
42 | long folderId = 0;
43 | var fileList = await cloud.GetFolderList(parent_id);
44 | Assert.IsTrue(fileList.code == LanZouCode.SUCCESS);
45 | var folder = fileList.folders.Find(a => a.name == name);
46 | if (folder == null)
47 | {
48 | var create = await cloud.CreateFolder(name, parent_id);
49 | Assert.IsTrue(create.code == LanZouCode.SUCCESS);
50 | folderId = create.id;
51 | }
52 | else
53 | {
54 | folderId = folder.id;
55 | }
56 | return folderId;
57 | }
58 |
59 | public static async Task GetTestFileBig(LanZouCloud cloud)
60 | {
61 | return await GetTestFile(cloud, TestFileBigPath);
62 | }
63 |
64 | public static async Task GetTestFile(LanZouCloud cloud)
65 | {
66 | return await GetTestFile(cloud, TestFilePath);
67 | }
68 |
69 | private static async Task GetTestFile(LanZouCloud cloud, string filepath)
70 | {
71 | long fileId = 0;
72 | var fileList = await cloud.GetFileList();
73 | Assert.IsTrue(fileList.code == LanZouCode.SUCCESS);
74 | var file = fileList.files.Find(a => a.name == Path.GetFileName(filepath));
75 | if (file == null)
76 | {
77 | var create = await cloud.UploadFile(filepath);
78 | Assert.IsTrue(create.code == LanZouCode.SUCCESS);
79 | fileId = create.id;
80 | }
81 | else
82 | {
83 | fileId = file.id;
84 | }
85 | return fileId;
86 | }
87 |
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tests/LanZouCloud.Tests/CommonTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System.Threading.Tasks;
3 | using LanZouCloudAPI;
4 | using System;
5 |
6 | namespace Test
7 | {
8 | [TestClass]
9 | public class CommonTest
10 | {
11 | [TestMethod]
12 | public async Task Login()
13 | {
14 | await AConfig.AsyncLoginCloud(true);
15 | }
16 |
17 | [TestMethod]
18 | public async Task Logout()
19 | {
20 | var cloud = await AConfig.AsyncLoginCloud(true);
21 | var result = await cloud.Logout();
22 | Assert.IsTrue(result.code == LanZouCode.SUCCESS);
23 | }
24 |
25 | [TestMethod]
26 | public async Task GetFileList()
27 | {
28 | var cloud = await AConfig.AsyncLoginCloud();
29 | var fileList = await cloud.GetFileList();
30 | Assert.IsTrue(fileList.code == LanZouCode.SUCCESS);
31 | Assert.IsTrue(fileList.files != null);
32 | }
33 |
34 | [TestMethod]
35 | public async Task GetFolderList()
36 | {
37 | var cloud = await AConfig.AsyncLoginCloud();
38 | var fileList = await cloud.GetFolderList();
39 | Assert.IsTrue(fileList.code == LanZouCode.SUCCESS);
40 | Assert.IsTrue(fileList.folders != null);
41 | }
42 |
43 | [TestMethod]
44 | public async Task GetFileInfoById()
45 | {
46 | var cloud = await AConfig.AsyncLoginCloud();
47 | var fileList = await cloud.GetFileList();
48 | Assert.IsTrue(fileList.code == LanZouCode.SUCCESS);
49 |
50 | var fileInfo = await cloud.GetFileInfo(fileList.files[0].id);
51 | Assert.IsTrue(fileInfo.code == LanZouCode.SUCCESS);
52 | Assert.IsTrue(!string.IsNullOrEmpty(fileInfo.durl));
53 | }
54 |
55 | [TestMethod]
56 | public async Task GetFolderInfoById()
57 | {
58 | var cloud = await AConfig.AsyncLoginCloud();
59 | var folderList = await cloud.GetFolderList();
60 | Assert.IsTrue(folderList.code == LanZouCode.SUCCESS);
61 |
62 | Assert.IsTrue(folderList.folders.Count > 0);
63 | var folderInfo = await cloud.GetFolderInfo(folderList.folders[0].id, 1, 2);
64 | Assert.IsTrue(folderInfo.code == LanZouCode.SUCCESS);
65 | Assert.IsTrue(!string.IsNullOrEmpty(folderInfo.name));
66 | }
67 |
68 | [TestMethod]
69 | public async Task GetFolderInfoByUrl()
70 | {
71 | var cloud = AConfig.Cloud();
72 | var url = "https://psyduck.lanzoui.com/b00tthepa";
73 | var folderInfo = await cloud.GetFolderInfoByUrl(url);
74 | Assert.IsTrue(folderInfo.code == LanZouCode.SUCCESS);
75 | Assert.IsTrue(!string.IsNullOrEmpty(folderInfo.name));
76 | }
77 |
78 | [TestMethod]
79 | public async Task RenameFolder()
80 | {
81 | var cloud = await AConfig.AsyncLoginCloud();
82 |
83 | var folderId = await AConfig.GetTestFolder(cloud);
84 |
85 | var info = await cloud.RenameFolder(folderId, AConfig.TestFolder + "_Rename");
86 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
87 |
88 | // revert
89 | info = await cloud.RenameFolder(folderId, AConfig.TestFolder);
90 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
91 | }
92 |
93 |
94 | [TestMethod]
95 | public async Task RenameFile()
96 | {
97 | var cloud = await AConfig.AsyncLoginCloud();
98 |
99 | var fileId = await AConfig.GetTestFile(cloud);
100 |
101 | var info = await cloud.RenameFile(fileId, AConfig.TestFolder + "_Rename");
102 |
103 | if (info.code == LanZouCode.FAILED && info.message == "此功能仅会员使用,请先开通会员")
104 | {
105 | return;
106 | }
107 |
108 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
109 |
110 | // revert
111 | info = await cloud.RenameFolder(fileId, AConfig.TestFolder);
112 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
113 | }
114 |
115 |
116 | [TestMethod]
117 | public async Task MoveFolder()
118 | {
119 | var cloud = await AConfig.AsyncLoginCloud();
120 |
121 | var folderId = await AConfig.GetTestFolder(cloud);
122 | var subF = await AConfig.GetTestFolder(cloud, "SUBBBBBBBBBBBBBBB", folderId);
123 |
124 | // upload
125 | var create = await cloud.UploadFile(AConfig.TestFilePath, null, subF);
126 | Assert.IsTrue(create.code == LanZouCode.SUCCESS);
127 |
128 | // move
129 | var info = await cloud.MoveFolder(subF, -1);
130 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
131 |
132 | // delete
133 | var del = await cloud.DeleteFolder(info.id);
134 | Assert.IsTrue(del.code == LanZouCode.SUCCESS);
135 | }
136 |
137 |
138 | // TODO: more unit tests
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/tests/LanZouCloud.Tests/DownloadTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using LanZouCloudAPI;
5 | using System;
6 | using System.Threading;
7 |
8 | namespace Test
9 | {
10 | [TestClass]
11 | public class DownloadTest
12 | {
13 | [TestMethod]
14 | public async Task DownloadFileById()
15 | {
16 | var cloud = await AConfig.AsyncLoginCloud();
17 | var fileList = await cloud.GetFileList();
18 | Assert.IsTrue(fileList.code == LanZouCode.SUCCESS);
19 |
20 | bool isStartOK = false;
21 | bool isReadyOK = false;
22 | bool isDownloadingOK = false;
23 | bool isFinishOK = false;
24 |
25 | var fileId = await AConfig.GetTestFile(cloud);
26 |
27 | var info = await cloud.DownloadFile(fileId, "./", null, true,
28 | new Progress(_progress =>
29 | {
30 | if (_progress.state == ProgressState.Start)
31 | isStartOK = true;
32 | if (_progress.state == ProgressState.Ready)
33 | isReadyOK = true;
34 | if (_progress.state == ProgressState.Progressing)
35 | isDownloadingOK = true;
36 | if (_progress.state == ProgressState.Finish)
37 | isFinishOK = true;
38 | }));
39 |
40 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
41 | Assert.IsTrue(!string.IsNullOrEmpty(info.url));
42 | Assert.IsTrue(!string.IsNullOrEmpty(info.fileName));
43 | Assert.IsTrue(File.Exists(info.filePath));
44 |
45 | Assert.IsTrue(isStartOK);
46 | Assert.IsTrue(isReadyOK);
47 | Assert.IsTrue(isDownloadingOK);
48 | Assert.IsTrue(isFinishOK);
49 | }
50 |
51 | [TestMethod]
52 | public async Task DownloadFileBigById()
53 | {
54 | var cloud = await AConfig.AsyncLoginCloud();
55 | var fileList = await cloud.GetFileList();
56 | Assert.IsTrue(fileList.code == LanZouCode.SUCCESS);
57 |
58 | var fileId = await AConfig.GetTestFileBig(cloud);
59 |
60 | var info = await cloud.DownloadFile(fileId, "./", null, true);
61 |
62 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
63 | }
64 |
65 | [TestMethod]
66 | public async Task DownloadFileBigAndCancelById()
67 | {
68 | var cloud = await AConfig.AsyncLoginCloud();
69 | var fileList = await cloud.GetFileList();
70 | Assert.IsTrue(fileList.code == LanZouCode.SUCCESS);
71 |
72 | var fileId = await AConfig.GetTestFileBig(cloud);
73 |
74 | var cts = new CancellationTokenSource();
75 |
76 | new Task(async () =>
77 | {
78 | await Task.Delay(4000);
79 | cts.Cancel();
80 | }).Start();
81 |
82 | var info = await cloud.DownloadFile(fileId, "./", null, true, null, cts.Token);
83 |
84 | Assert.IsTrue(info.code == LanZouCode.TASK_CANCELED);
85 | }
86 |
87 | [TestMethod]
88 | public async Task DownloadFileByUrl()
89 | {
90 | var cloud = AConfig.Cloud();
91 |
92 | bool isStartOK = false;
93 | bool isReadyOK = false;
94 | bool isDownloadingOK = false;
95 | bool isFinishOK = false;
96 | var url = "https://psyduck.lanzoui.com/ibf1Xvlo4rc";
97 | // var url = "https://psyduck.lanzoui.com/idyVcwcu06f";
98 | var info = await cloud.DownloadFileByUrl(url, "./", null, "95w3", true,
99 | new Progress(_progress =>
100 | {
101 | if (_progress.state == ProgressState.Start)
102 | isStartOK = true;
103 | if (_progress.state == ProgressState.Ready)
104 | isReadyOK = true;
105 | if (_progress.state == ProgressState.Progressing)
106 | isDownloadingOK = true;
107 | if (_progress.state == ProgressState.Finish)
108 | isFinishOK = true;
109 | }));
110 |
111 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
112 | Assert.IsTrue(!string.IsNullOrEmpty(info.url));
113 | Assert.IsTrue(!string.IsNullOrEmpty(info.fileName));
114 | Assert.IsTrue(File.Exists(info.filePath));
115 |
116 | Assert.IsTrue(isStartOK);
117 | Assert.IsTrue(isReadyOK);
118 | Assert.IsTrue(isDownloadingOK);
119 | Assert.IsTrue(isFinishOK);
120 | }
121 |
122 | [TestMethod]
123 | public async Task DownloadFileBigByUrl()
124 | {
125 | var cloud = AConfig.Cloud();
126 |
127 | bool isStartOK = false;
128 | bool isReadyOK = false;
129 | bool isDownloadingOK = false;
130 | bool isFinishOK = false;
131 | var url = "https://psyduck.lanzoui.com/iDCSnvmho6f";
132 | var info = await cloud.DownloadFileByUrl(url, "./", null, null, true,
133 | new Progress(_progress =>
134 | {
135 | if (_progress.state == ProgressState.Start)
136 | isStartOK = true;
137 | if (_progress.state == ProgressState.Ready)
138 | isReadyOK = true;
139 | if (_progress.state == ProgressState.Progressing)
140 | isDownloadingOK = true;
141 | if (_progress.state == ProgressState.Finish)
142 | isFinishOK = true;
143 | }));
144 |
145 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
146 | Assert.IsTrue(!string.IsNullOrEmpty(info.url));
147 | Assert.IsTrue(!string.IsNullOrEmpty(info.fileName));
148 | Assert.IsTrue(File.Exists(info.filePath));
149 |
150 | Assert.IsTrue(isStartOK);
151 | Assert.IsTrue(isReadyOK);
152 | Assert.IsTrue(isDownloadingOK);
153 | Assert.IsTrue(isFinishOK);
154 | }
155 |
156 |
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/tests/LanZouCloud.Tests/LanZouCloud.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/LanZouCloud.Tests/UploadTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using LanZouCloudAPI;
5 | using System;
6 | using System.Threading;
7 |
8 | namespace Test
9 | {
10 | [TestClass]
11 | public class UploadTest
12 | {
13 | [TestMethod]
14 | public async Task UploadFile()
15 | {
16 | var cloud = await AConfig.AsyncLoginCloud();
17 |
18 | bool isStartOK = false;
19 | bool isReadyOK = false;
20 | bool isUploadingOK = false;
21 | bool isFinishOK = false;
22 |
23 | var info = await cloud.UploadFile(AConfig.TestFilePath, null, -1, true,
24 | new Progress(_progress =>
25 | {
26 | if (_progress.state == ProgressState.Start)
27 | isStartOK = true;
28 | if (_progress.state == ProgressState.Ready)
29 | isReadyOK = true;
30 | if (_progress.state == ProgressState.Progressing)
31 | isUploadingOK = true;
32 | if (_progress.state == ProgressState.Finish)
33 | isFinishOK = true;
34 | }));
35 |
36 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
37 | Assert.IsTrue(!string.IsNullOrEmpty(info.url));
38 | Assert.IsTrue(!string.IsNullOrEmpty(info.fileName));
39 | Assert.IsTrue(info.id != 0);
40 |
41 | Assert.IsTrue(isStartOK);
42 | Assert.IsTrue(isReadyOK);
43 | Assert.IsTrue(isUploadingOK);
44 | Assert.IsTrue(isFinishOK);
45 | }
46 |
47 |
48 | [TestMethod]
49 | public async Task UploadFileBigAndCancel()
50 | {
51 | var cloud = await AConfig.AsyncLoginCloud();
52 | var cts = new CancellationTokenSource();
53 | new Task(async () =>
54 | {
55 | await Task.Delay(4000);
56 | cts.Cancel();
57 | }).Start();
58 | var info = await cloud.UploadFile(AConfig.TestFileBigPath, null, -1, false, null, cts.Token);
59 | Assert.IsTrue(info.code == LanZouCode.TASK_CANCELED);
60 | }
61 |
62 | [TestMethod]
63 | public async Task UploadFileBig()
64 | {
65 | var cloud = await AConfig.AsyncLoginCloud();
66 | var info = await cloud.UploadFile(AConfig.TestFileBigPath, null, -1, true);
67 | Assert.IsTrue(info.code == LanZouCode.SUCCESS);
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------