├── .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 | --------------------------------------------------------------------------------