├── .nuget ├── NuGet.exe ├── NuGet.Config └── NuGet.targets ├── WebDAVClient.sln.DotSettings ├── WebDAVClient ├── HttpClient │ ├── IHttpClientWrapper.cs │ └── HttpClientWrapper.cs ├── Model │ └── Item.cs ├── Helpers │ ├── WebDAVConflictException.cs │ ├── WebDAVException.cs │ └── ResponseParser.cs ├── WebDAVClient.csproj ├── IClient.cs └── Client.cs ├── TestWebDAVClient ├── TestWebDAVClient.csproj └── Program.cs ├── .github └── workflows │ └── manual.yml ├── WebDAVClient.sln ├── .gitignore ├── README.md ├── LICENSE.md └── ROADMAP.md /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saguiitay/WebDAVClient/HEAD/.nuget/NuGet.exe -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /WebDAVClient.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | DAV -------------------------------------------------------------------------------- /WebDAVClient/HttpClient/IHttpClientWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace WebDAVClient.HttpClient 6 | { 7 | public interface IHttpClientWrapper 8 | { 9 | Task SendAsync(HttpRequestMessage request, HttpCompletionOption responseHeadersRead, CancellationToken cancellationToken = default); 10 | Task SendUploadAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); 11 | } 12 | } -------------------------------------------------------------------------------- /WebDAVClient/Model/Item.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebDAVClient.Model 4 | { 5 | public class Item 6 | { 7 | public string Href { get; set; } 8 | public DateTime? CreationDate { get; set; } 9 | public string Etag { get; set; } 10 | public bool IsHidden { get; set; } 11 | public bool IsCollection { get; set; } 12 | public string ContentType { get; set; } 13 | public DateTime? LastModified { get; set; } 14 | public string DisplayName { get; set; } 15 | public long? ContentLength { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TestWebDAVClient/TestWebDAVClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | Debug;Release;Release-Unsigned 6 | 7 | 8 | DEBUG;TRACE 9 | 10 | 11 | 12 | {F21EB154-C84E-4390-87CF-271E3DF4F059} 13 | WebDAVClient 14 | 15 | 16 | -------------------------------------------------------------------------------- /WebDAVClient/Helpers/WebDAVConflictException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebDAVClient.Helpers 4 | { 5 | public class WebDAVConflictException : WebDAVException 6 | { 7 | public WebDAVConflictException() 8 | { 9 | } 10 | 11 | public WebDAVConflictException(string message) 12 | : base(message) 13 | {} 14 | 15 | public WebDAVConflictException(string message, int hr) 16 | : base(message, hr) 17 | {} 18 | 19 | public WebDAVConflictException(string message, Exception innerException) 20 | : base(message, innerException) 21 | {} 22 | 23 | public WebDAVConflictException(int httpCode, string message, Exception innerException) 24 | : base(httpCode, message, innerException) 25 | {} 26 | 27 | public WebDAVConflictException(int httpCode, string message) 28 | : base(httpCode, message) 29 | {} 30 | 31 | public WebDAVConflictException(int httpCode, string message, int hr) 32 | : base(httpCode, message, hr) 33 | {} 34 | } 35 | } -------------------------------------------------------------------------------- /.github/workflows/manual.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow that is manually triggered 2 | 3 | name: Manual workflow 4 | 5 | # Controls when the action will run. Workflow runs when manually triggered using the UI 6 | # or API. 7 | on: 8 | workflow_dispatch: 9 | 10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 11 | jobs: 12 | # This workflow contains a single job called "greet" 13 | publish: 14 | # The type of runner that the job will run on 15 | runs-on: ubuntu-latest 16 | 17 | # Steps represent a sequence of tasks that will be executed as part of the job 18 | steps: 19 | - uses: actions/checkout@v5 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v5 22 | with: 23 | dotnet-version: | 24 | 8.0.x 25 | 9.0.x 26 | - name: Restore dependencies 27 | run: dotnet restore 28 | - name: Build 29 | run: dotnet build -c Release --no-restore 30 | - name: Push to NuGet 31 | run: | 32 | dotnet pack -c Release -o $PWD/Release/nuget 33 | for file in Release/nuget/WebDAVClient.*.nupkg; do dotnet nuget push -s https://api.nuget.org/v3/index.json -k ${{secrets.NUGET_API_KEY}} $file; done 34 | -------------------------------------------------------------------------------- /WebDAVClient.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.33913.275 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebDAVClient", "WebDAVClient\WebDAVClient.csproj", "{F21EB154-C84E-4390-87CF-271E3DF4F059}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestWebDAVClient", "TestWebDAVClient\TestWebDAVClient.csproj", "{C8999849-83F9-4B2A-AFD6-1C6ED43A4B87}" 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 | {F21EB154-C84E-4390-87CF-271E3DF4F059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {F21EB154-C84E-4390-87CF-271E3DF4F059}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {F21EB154-C84E-4390-87CF-271E3DF4F059}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {F21EB154-C84E-4390-87CF-271E3DF4F059}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {C8999849-83F9-4B2A-AFD6-1C6ED43A4B87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {C8999849-83F9-4B2A-AFD6-1C6ED43A4B87}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {C8999849-83F9-4B2A-AFD6-1C6ED43A4B87}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {C8999849-83F9-4B2A-AFD6-1C6ED43A4B87}.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 = {A49CDF2B-0E22-490F-97BF-6F614A4445B0} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /WebDAVClient/WebDAVClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | Debug;Release;Release-Unsigned 6 | true 7 | WebDAVClient 8 | 2.3.0 9 | WebDAVClient is a strongly-typed, async, C# client for WebDAV. 10 | Sagui Itay 11 | 12 | https://github.com/saguiitay/WebDAVClient 13 | README.md 14 | Copyright © 2025 Sagui Itay 15 | WebDAV HttpClient c# 16 | * Implement `IDisposable` to avoid `HttpClient` leak 17 | * Added support for `CancellationToken` 18 | * Various performance improvements to reduce memory allocations 19 | * Minor code cleanup 20 | 2.3.0.0 21 | 2.3.0.0 22 | MIT 23 | true 24 | snupkg 25 | 26 | 27 | 28 | DEBUG;TRACE 29 | 30 | 31 | 32 | pdbonly 33 | True 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /WebDAVClient/Helpers/WebDAVException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebDAVClient.Helpers 4 | { 5 | public class WebDAVException : Exception 6 | { 7 | private readonly int m_httpCode; 8 | 9 | public int ErrorCode { get; } 10 | 11 | public WebDAVException() 12 | { 13 | } 14 | 15 | public WebDAVException(string message) 16 | : base(message) 17 | {} 18 | 19 | public WebDAVException(string message, int hr) 20 | : base(message) 21 | { 22 | ErrorCode = hr; 23 | } 24 | 25 | public WebDAVException(string message, Exception innerException) 26 | : base(message, innerException) 27 | {} 28 | 29 | public WebDAVException(int httpCode, string message, Exception innerException) 30 | : base(message, innerException) 31 | { 32 | m_httpCode = httpCode; 33 | } 34 | 35 | public WebDAVException(int httpCode, string message) 36 | : base(message) 37 | { 38 | m_httpCode = httpCode; 39 | } 40 | 41 | public WebDAVException(int httpCode, string message, int hr) 42 | : base(message) 43 | { 44 | m_httpCode = httpCode; 45 | ErrorCode = hr; 46 | } 47 | 48 | public int GetHttpCode() 49 | { 50 | return m_httpCode; 51 | } 52 | 53 | public override string ToString() 54 | { 55 | var s = string.Format("HttpStatusCode: {0}", GetHttpCode()); 56 | s += Environment.NewLine + string.Format("ErrorCode: {0}", ErrorCode); 57 | s += Environment.NewLine + string.Format("Message: {0}", Message); 58 | s += Environment.NewLine + base.ToString(); 59 | 60 | return s; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /WebDAVClient/HttpClient/HttpClientWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace WebDAVClient.HttpClient 7 | { 8 | public class HttpClientWrapper : IHttpClientWrapper, IDisposable 9 | { 10 | private System.Net.Http.HttpClient m_httpClient; 11 | private System.Net.Http.HttpClient m_uploadHttpClient; 12 | private bool m_disposedValue; 13 | 14 | public HttpClientWrapper(System.Net.Http.HttpClient httpClient, System.Net.Http.HttpClient uploadHttpClient = null) 15 | { 16 | m_httpClient = httpClient; 17 | m_uploadHttpClient = uploadHttpClient ?? httpClient; 18 | } 19 | 20 | public Task SendAsync(HttpRequestMessage request, HttpCompletionOption responseHeadersRead, CancellationToken cancellationToken = default) 21 | { 22 | return m_httpClient.SendAsync(request, responseHeadersRead, cancellationToken); 23 | } 24 | 25 | public Task SendUploadAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) 26 | { 27 | return m_uploadHttpClient.SendAsync(request, cancellationToken); 28 | } 29 | 30 | 31 | #region IDisposable methods 32 | protected virtual void Dispose(bool disposing) 33 | { 34 | if (!m_disposedValue) 35 | { 36 | if (disposing) 37 | { 38 | m_httpClient.Dispose(); 39 | if (m_httpClient != m_uploadHttpClient) 40 | { 41 | m_uploadHttpClient.Dispose(); 42 | } 43 | m_httpClient = null; 44 | m_uploadHttpClient = null; 45 | } 46 | 47 | m_disposedValue = true; 48 | } 49 | } 50 | 51 | public void Dispose() 52 | { 53 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 54 | Dispose(disposing: true); 55 | GC.SuppressFinalize(this); 56 | } 57 | #endregion 58 | } 59 | } -------------------------------------------------------------------------------- /TestWebDAVClient/Program.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using WebDAVClient; 6 | 7 | namespace TestWebDAVClient 8 | { 9 | class Program 10 | { 11 | private static void Main(string[] args) 12 | { 13 | MainAsync().Wait(); 14 | } 15 | 16 | 17 | private static async Task MainAsync() 18 | { 19 | // Basic authentication required 20 | IClient c = new Client(new NetworkCredential { UserName = "USERNAME" , Password = "PASSWORD"}); 21 | c.Server = "https://dav.dumptruck.goldenfrog.com/"; 22 | c.BasePath = "/dav/"; 23 | 24 | // List items in the root folder 25 | var files = await c.List(); 26 | // Find first folder in the root folder 27 | var folder = files.FirstOrDefault(f => f.Href.EndsWith("/Test/")); 28 | // Load a specific folder 29 | var folderReloaded = await c.GetFolder(folder.Href); 30 | 31 | // List items in the folder 32 | var folderFiles = await c.List(folderReloaded.Href); 33 | // Find a file in the folder 34 | var folderFile = folderFiles.FirstOrDefault(f => f.IsCollection == false); 35 | 36 | var tempFileName = Path.GetTempFileName(); 37 | 38 | // Download item into a temporary file 39 | using (var tempFile = File.OpenWrite(tempFileName)) 40 | using (var stream = await c.Download(folderFile.Href)) 41 | await stream.CopyToAsync(tempFile); 42 | 43 | // Update file back to webdav 44 | var tempName = Path.GetRandomFileName(); 45 | using (var fileStream = File.OpenRead(tempFileName)) 46 | { 47 | var fileUploaded = await c.Upload(folder.Href, fileStream, tempName); 48 | 49 | var stream = new MemoryStream(new byte[] {0xF0, 0x0B, 0xA5}); 50 | fileUploaded = await c.UploadPartial(folder.Href, stream, tempName, 2, 6); 51 | if (fileUploaded) 52 | { 53 | var parts = await c.DownloadPartial(folderFile.Href, 2, 6); 54 | } 55 | } 56 | 57 | // Create a folder 58 | var tempFolderName = Path.GetRandomFileName(); 59 | var isfolderCreated = await c.CreateDir("/", tempFolderName); 60 | 61 | // Delete created folder 62 | var folderCreated = await c.GetFolder("/" + tempFolderName); 63 | await c.DeleteFolder(folderCreated.Href); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | .vs/ 9 | 10 | # Build results 11 | [Dd]ebug/ 12 | [Dd]ebugPublic/ 13 | [Rr]elease/ 14 | [Rr]eleases/ 15 | x64/ 16 | x86/ 17 | build/ 18 | bld/ 19 | [Bb]in/ 20 | [Oo]bj/ 21 | *.userprefs 22 | 23 | # Roslyn cache directories 24 | *.ide/ 25 | 26 | # MSTest test Results 27 | [Tt]est[Rr]esult*/ 28 | [Bb]uild[Ll]og.* 29 | 30 | #NUNIT 31 | *.VisualState.xml 32 | TestResult.xml 33 | 34 | # Build Results of an ATL Project 35 | [Dd]ebugPS/ 36 | [Rr]eleasePS/ 37 | dlldata.c 38 | 39 | *_i.c 40 | *_p.c 41 | *_i.h 42 | *.ilk 43 | *.meta 44 | *.obj 45 | *.pch 46 | *.pdb 47 | *.pgc 48 | *.pgd 49 | *.rsp 50 | *.sbr 51 | *.tlb 52 | *.tli 53 | *.tlh 54 | *.tmp 55 | *.tmp_proj 56 | *.log 57 | *.vspscc 58 | *.vssscc 59 | .builds 60 | *.pidb 61 | *.svclog 62 | *.scc 63 | 64 | # Chutzpah Test files 65 | _Chutzpah* 66 | 67 | # Visual C++ cache files 68 | ipch/ 69 | *.aps 70 | *.ncb 71 | *.opensdf 72 | *.sdf 73 | *.cachefile 74 | 75 | # Visual Studio profiler 76 | *.psess 77 | *.vsp 78 | *.vspx 79 | 80 | # TFS 2012 Local Workspace 81 | $tf/ 82 | 83 | # Guidance Automation Toolkit 84 | *.gpState 85 | 86 | # ReSharper is a .NET coding add-in 87 | _ReSharper*/ 88 | *.[Rr]e[Ss]harper 89 | *.DotSettings.user 90 | 91 | # JustCode is a .NET coding addin-in 92 | .JustCode 93 | 94 | # TeamCity is a build add-in 95 | _TeamCity* 96 | 97 | # DotCover is a Code Coverage Tool 98 | *.dotCover 99 | 100 | # NCrunch 101 | _NCrunch_* 102 | .*crunch*.local.xml 103 | 104 | # MightyMoose 105 | *.mm.* 106 | AutoTest.Net/ 107 | 108 | # Web workbench (sass) 109 | .sass-cache/ 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.[Pp]ublish.xml 129 | *.azurePubxml 130 | # TODO: Comment the next line if you want to checkin your web deploy settings 131 | # but database connection strings (with potential passwords) will be unencrypted 132 | *.pubxml 133 | *.publishproj 134 | 135 | # NuGet Packages 136 | *.nupkg 137 | *.nupkg.bak 138 | # The packages folder can be ignored because of Package Restore 139 | **/packages/* 140 | # except build/, which is used as an MSBuild target. 141 | !**/packages/build/ 142 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 143 | #!**/packages/repositories.config 144 | 145 | # Windows Azure Build Output 146 | csx/ 147 | *.build.csdef 148 | 149 | # Windows Store app package directory 150 | AppPackages/ 151 | 152 | # Others 153 | sql/ 154 | *.Cache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | 165 | # RIA/Silverlight projects 166 | Generated_Code/ 167 | 168 | # Backup & report files from converting an old project file 169 | # to a newer Visual Studio version. Backup files are not needed, 170 | # because we have git ;-) 171 | _UpgradeReport_Files/ 172 | Backup*/ 173 | UpgradeLog*.XML 174 | UpgradeLog*.htm 175 | 176 | # SQL Server files 177 | *.mdf 178 | *.ldf 179 | 180 | # Business Intelligence projects 181 | *.rdl.data 182 | *.bim.layout 183 | *.bim_*.settings 184 | 185 | # Microsoft Fakes 186 | FakesAssemblies/ 187 | 188 | #NuGet 189 | packages 190 | !packages/repositories.config 191 | /.localhistory 192 | /.vs 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebDAVClient 2 | 3 | ## Overview 4 | 5 | WebDAV Client for .Net Core, strongly typed, async and open-sourced, imnplemented in C#. 6 | 7 | WebDAVClient is based originally on . I've added Async support (instead of Callback), as well strong-types responses. 8 | 9 | ## NuGet 10 | 11 | WebDAVClient is available as a [NuGet package](https://www.nuget.org/packages/WebDAVClient/) 12 | 13 | ## Features 14 | 15 | * Available as a NuGet packages 16 | * Fully support Async/Await 17 | * Strong-typed 18 | * Implemented using HttpClient, which means support for extendibility such as throttling and monitoring 19 | * Supports Unauthenticated or Windows Authentication-based access 20 | * Supports custom Certificate validation 21 | * Supports the full WebDAV API: 22 | * Retrieving files & folders 23 | * Listing items in a folder 24 | * Creating & deleting folders 25 | * Downloading & uploading files 26 | * Downloading & uploading partial content 27 | * Moving & copying files and folders 28 | 29 | ## Release Notes 30 | 31 | + **2.2.1** Minor packaging improvements 32 | + **2.2.0** Improvement: 33 | - Implement `IDisposable` to avoid `HttpClient` leak 34 | - Added support for `CancellationToken` 35 | - Various performance improvements to reduce memory allocations 36 | - Minor code cleanup 37 | + **2.1.0** Bug fixes: Fixed handling of port 38 | + **2.0.0** BREAKING CHANGES!!! 39 | - Added support for .Net Core 3.0 & .Net Standard 2.0 40 | + **1.1.3** Improvement: 41 | - Ignore WhiteSpaces while parsing response XML 42 | - Enable Windows Authentication 43 | - Support curstom certificate validation 44 | - Add download partial content 45 | - Improved testability by using a wrapper for HttpClient 46 | + **1.1.2** Improvements: 47 | - Make WebDAVClient work on Mono and make NuGet compatible with Xamarin projects 48 | - Provide a IWebProxy parameter for HttpClient 49 | - Change type of ContentLength from int to long 50 | - Improved compatibility to SVN 51 | + **1.1.1** Improvements: 52 | - Improved parsing of server responses 53 | - Improved the way we calculate URLs 54 | + **1.1.0** Improvement: Improved parsing of server values 55 | + **1.0.23** Improvement: Improve the way we handle path with "invalid" characters 56 | + **1.0.22** Bug fixes and improvements: 57 | - Improved the way we identify the root folder 58 | - Fixed calculation of URLs 59 | + **1.0.20** Improvements: 60 | - Added support for default/custom UserAgent 61 | - Improved ToString method of WebDAVException 62 | + **1.0.19** Improvement: Added support for uploads timeout 63 | + **1.0.18** Improvement: Added MoveFolder and MoveFile methods 64 | + **1.0.17** Improvement: Added DeleteFolder and DeleteFile methods 65 | + **1.0.16** Improvements: 66 | - Improved filtering of listed folder 67 | - Disable ExpectContinue header from requests 68 | + **1.0.15** Bug fixes: Trim trailing slashes from HRef of non-collection (files) items 69 | + **1.0.14** BREAKING CHANGES: Replaced Get() method with separated GetFolder() and GetFile() methods. 70 | + **1.0.13** Improvement: Replaced deserialization mechanism with a more fault-tolerant one. 71 | + **1.0.12** Bug fixes: Removed disposing of HttpMessageResponse when returning content as Stream 72 | + **1.0.11** Improvements: 73 | - Introduced new IClient interface 74 | - Added new WebDAVConflictException 75 | - Dispose HttpRequestMessage and HttpResponseMessage after use 76 | + **1.0.10** Bug fixes: Correctly handle NoContent server responses as valid responses 77 | + **1.0.9** Bug fixes: Improved handling of BasePath 78 | + **1.0.8** Bug fixes: Handle cases when CreationDate is null 79 | + **1.0.7** Async improvements: Added ConfigureAwait(false) to all async calls 80 | + **1.0.5** Bug fixes and improvements: 81 | - Decode Href property of files/folders 82 | - Complete Http send operation as soon as headers are read 83 | + **1.0.3** Various bug fixes. 84 | + **1.0.1** Improved error handling and authentication 85 | + **1.0.0** Initial release. 86 | 87 | 88 | # Usage 89 | 90 | ``` csharp 91 | // Basic authentication required 92 | IClient c = new Client(new NetworkCredential { UserName = "USERNAME" , Password = "PASSWORD"}); 93 | // OR without authentication 94 | var client = new WebDAVClient.Client(new NetworkCredential()); 95 | 96 | // Set basic information for WebDAV provider 97 | c.Server = "https://dav.dumptruck.goldenfrog.com/"; 98 | c.BasePath = "/dav/"; 99 | 100 | // List items in the root folder 101 | var files = await c.List(); 102 | 103 | // Find folder named 'Test' 104 | var folder = files.FirstOrDefault(f => f.Href.EndsWith("/Test/")); 105 | // Reload folder 'Test' 106 | var folderReloaded = await c.GetFolder(folder.Href); 107 | 108 | // Retrieve list of items in 'Test' folder 109 | var folderFiles = await c.List(folderReloaded.Href); 110 | // Find first file in 'Test' folder 111 | var folderFile = folderFiles.FirstOrDefault(f => f.IsCollection == false); 112 | 113 | var tempFileName = Path.GetTempFileName(); 114 | 115 | // Download item into a temporary file 116 | using (var tempFile = File.OpenWrite(tempFileName)) 117 | using (var stream = await c.Download(folderFile.Href)) 118 | await stream.CopyToAsync(tempFile); 119 | 120 | // Update file back to webdav 121 | var tempName = Path.GetRandomFileName(); 122 | using (var fileStream = File.OpenRead(tempFileName)) 123 | { 124 | var fileUploaded = await c.Upload(folder.Href, fileStream, tempName); 125 | } 126 | 127 | // Create a folder 128 | var tempFolderName = Path.GetRandomFileName(); 129 | var isfolderCreated = await c.CreateDir("/", tempFolderName); 130 | 131 | // Delete created folder 132 | var folderCreated = await c.GetFolder("/" + tempFolderName); 133 | await c.DeleteFolder(folderCreated.Href); 134 | ``` 135 | 136 | ## Contact 137 | 138 | You can contact me on twitter [@saguiitay](https://twitter.com/saguiitay). 139 | -------------------------------------------------------------------------------- /WebDAVClient/IClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using WebDAVClient.Model; 7 | 8 | namespace WebDAVClient 9 | { 10 | public interface IClient : IDisposable 11 | { 12 | /// 13 | /// Specify the WebDAV hostname (required). 14 | /// 15 | string Server { get; set; } 16 | 17 | /// 18 | /// Specify the path of a WebDAV directory to use as 'root' (default: /) 19 | /// 20 | string BasePath { get; set; } 21 | 22 | /// 23 | /// Specify a port to use 24 | /// 25 | int? Port { get; set; } 26 | 27 | /// 28 | /// Specify the UserAgent (and UserAgent version) string to use in requests 29 | /// 30 | string UserAgent { get; set; } 31 | 32 | /// 33 | /// Specify the UserAgent (and UserAgent version) string to use in requests 34 | /// 35 | string UserAgentVersion { get; set; } 36 | 37 | /// 38 | /// Specify additional headers to be sent with every request 39 | /// 40 | ICollection> CustomHeaders { get; set; } 41 | 42 | /// 43 | /// List all files present on the server. 44 | /// 45 | /// List only files in this path 46 | /// Recursion depth 47 | /// A list of files (entries without a trailing slash) and directories (entries with a trailing slash) 48 | Task> List(string path = "/", int? depth = 1, CancellationToken cancellationToken = default); 49 | 50 | /// 51 | /// Get folder information from the server. 52 | /// 53 | /// An item representing the retrieved folder 54 | Task GetFolder(string path = "/", CancellationToken cancellationToken = default); 55 | 56 | /// 57 | /// Get file information from the server. 58 | /// 59 | /// An item representing the retrieved file 60 | Task GetFile(string path = "/", CancellationToken cancellationToken = default); 61 | 62 | /// 63 | /// Download a file from the server 64 | /// 65 | /// Source path and filename of the file on the server 66 | /// A stream with the content of the downloaded file 67 | Task Download(string remoteFilePath, CancellationToken cancellationToken = default); 68 | 69 | /// 70 | /// Download a part of file from the server 71 | /// 72 | /// Source path and filename of the file on the server 73 | /// Start bytes of content 74 | /// End bytes of content 75 | /// A stream with the partial content of the downloaded file 76 | Task DownloadPartial(string remoteFilePath, long startBytes, long endBytes, CancellationToken cancellationToken = default); 77 | 78 | /// 79 | /// Upload a file to the server 80 | /// 81 | /// Source path and filename of the file on the server 82 | /// 83 | /// 84 | /// True if the file was uploaded successfully. False otherwise 85 | Task Upload(string remoteFilePath, Stream content, string name, CancellationToken cancellationToken = default); 86 | 87 | /// 88 | /// Upload a part of a file to the server. 89 | /// 90 | /// Target path excluding the servername and base path 91 | /// The content to upload. Must match the length of minus 92 | /// The target filename. The file must exist on the server 93 | /// StartByte on the target file 94 | /// EndByte on the target file 95 | /// True if the file part was uploaded successfully. False otherwise 96 | Task UploadPartial(string remoteFilePath, Stream content, string name, long startBytes, long endBytes, CancellationToken cancellationToken = default); 97 | 98 | /// 99 | /// Create a directory on the server 100 | /// 101 | /// Destination path of the directory on the server 102 | /// The name of the folder to create 103 | /// True if the folder was created successfully. False otherwise 104 | Task CreateDir(string remotePath, string name, CancellationToken cancellationToken = default); 105 | 106 | /// 107 | /// Deletes a folder from the server. 108 | /// 109 | Task DeleteFolder(string path = "/", CancellationToken cancellationToken = default); 110 | 111 | /// 112 | /// Deletes a file from the server. 113 | /// 114 | Task DeleteFile(string path = "/", CancellationToken cancellationToken = default); 115 | 116 | /// 117 | /// Move a folder on the server 118 | /// 119 | /// Source path of the folder on the server 120 | /// Destination path of the folder on the server 121 | /// True if the folder was moved successfully. False otherwise 122 | Task MoveFolder(string srcFolderPath, string dstFolderPath, CancellationToken cancellationToken = default); 123 | 124 | /// 125 | /// Move a file on the server 126 | /// 127 | /// Source path and filename of the file on the server 128 | /// Destination path and filename of the file on the server 129 | /// True if the file was moved successfully. False otherwise 130 | Task MoveFile(string srcFilePath, string dstFilePath, CancellationToken cancellationToken = default); 131 | 132 | /// 133 | /// Copies a folder on the server 134 | /// 135 | /// Source path of the folder on the server 136 | /// Destination path of the folder on the server 137 | /// True if the folder was copied successfully. False otherwise 138 | Task CopyFolder(string srcFolderPath, string dstFolderPath, CancellationToken cancellationToken = default); 139 | 140 | /// 141 | /// Copies a file on the server 142 | /// 143 | /// Source path and filename of the file on the server 144 | /// Destination path and filename of the file on the server 145 | /// True if the file was copied successfully. False otherwise 146 | Task CopyFile(string srcFilePath, string dstFilePath, CancellationToken cancellationToken = default); 147 | } 148 | } -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | 32 | 33 | 34 | 35 | $(SolutionDir).nuget 36 | 37 | 38 | 39 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config 40 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config 41 | 42 | 43 | 44 | $(MSBuildProjectDirectory)\packages.config 45 | $(PackagesProjectConfig) 46 | 47 | 48 | 49 | 50 | $(NuGetToolsPath)\NuGet.exe 51 | @(PackageSource) 52 | 53 | "$(NuGetExePath)" 54 | mono --runtime=v4.0.30319 "$(NuGetExePath)" 55 | 56 | $(TargetDir.Trim('\\')) 57 | 58 | -RequireConsent 59 | -NonInteractive 60 | 61 | "$(SolutionDir) " 62 | "$(SolutionDir)" 63 | 64 | 65 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) 66 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols 67 | 68 | 69 | 70 | RestorePackages; 71 | $(BuildDependsOn); 72 | 73 | 74 | 75 | 76 | $(BuildDependsOn); 77 | BuildPackage; 78 | 79 | 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /WebDAVClient/Helpers/ResponseParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Xml; 7 | using WebDAVClient.Model; 8 | 9 | namespace WebDAVClient.Helpers 10 | { 11 | /// 12 | /// Represents the parser for response's results. 13 | /// 14 | internal static class ResponseParser 15 | { 16 | /// 17 | /// Parses the disk item. 18 | /// 19 | /// The response text. 20 | /// The parsed item. 21 | public static Item ParseItem(Stream stream) 22 | { 23 | return ParseItems(stream).FirstOrDefault(); 24 | } 25 | 26 | internal static XmlReaderSettings XmlReaderSettings = new XmlReaderSettings 27 | { 28 | IgnoreComments = true, 29 | IgnoreProcessingInstructions = true, 30 | IgnoreWhitespace = true 31 | }; 32 | 33 | /// 34 | /// Parses the disk items. 35 | /// 36 | /// The response text. 37 | /// The list of parsed items. 38 | public static List ParseItems(Stream stream) 39 | { 40 | var items = new List(); 41 | using (var reader = XmlReader.Create(stream, XmlReaderSettings)) 42 | { 43 | Item itemInfo = null; 44 | while (reader.Read()) 45 | { 46 | if (reader.NodeType == XmlNodeType.Element) 47 | { 48 | switch (reader.LocalName.ToLower()) 49 | { 50 | case "response": 51 | itemInfo = new Item(); 52 | break; 53 | case "href": 54 | if (!reader.IsEmptyElement) 55 | { 56 | reader.Read(); 57 | var value = reader.Value; 58 | value = value.Replace("#", "%23"); 59 | itemInfo.Href = value; 60 | } 61 | break; 62 | case "creationdate": 63 | if (!reader.IsEmptyElement) 64 | { 65 | reader.Read(); 66 | if (DateTime.TryParse(reader.Value, out var creationDate)) 67 | { 68 | itemInfo.CreationDate = creationDate; 69 | } 70 | } 71 | break; 72 | case "getlastmodified": 73 | if (!reader.IsEmptyElement) 74 | { 75 | reader.Read(); 76 | if (DateTime.TryParse(reader.Value, out var lastModified)) 77 | { 78 | itemInfo.LastModified = lastModified; 79 | } 80 | } 81 | break; 82 | case "displayname": 83 | if (!reader.IsEmptyElement) 84 | { 85 | reader.Read(); 86 | itemInfo.DisplayName = reader.Value; 87 | } 88 | break; 89 | case "getcontentlength": 90 | if (!reader.IsEmptyElement) 91 | { 92 | reader.Read(); 93 | if (long.TryParse(reader.Value, out long contentLength)) 94 | { 95 | itemInfo.ContentLength = contentLength; 96 | } 97 | } 98 | break; 99 | case "getcontenttype": 100 | if (!reader.IsEmptyElement) 101 | { 102 | reader.Read(); 103 | itemInfo.ContentType = reader.Value; 104 | } 105 | break; 106 | case "getetag": 107 | if (!reader.IsEmptyElement) 108 | { 109 | reader.Read(); 110 | itemInfo.Etag = reader.Value; 111 | } 112 | break; 113 | case "iscollection": 114 | if (!reader.IsEmptyElement) 115 | { 116 | reader.Read(); 117 | if (bool.TryParse(reader.Value, out bool isCollection)) 118 | { 119 | itemInfo.IsCollection = isCollection; 120 | } 121 | if (int.TryParse(reader.Value, out int isCollectionInt)) 122 | { 123 | itemInfo.IsCollection = isCollectionInt == 1; 124 | } 125 | } 126 | break; 127 | case "resourcetype": 128 | if (!reader.IsEmptyElement) 129 | { 130 | reader.Read(); 131 | var resourceType = reader.LocalName.ToLower(); 132 | if (string.Equals(resourceType, "collection", StringComparison.InvariantCultureIgnoreCase)) 133 | { 134 | itemInfo.IsCollection = true; 135 | } 136 | } 137 | break; 138 | case "hidden": 139 | case "ishidden": 140 | { 141 | itemInfo.IsHidden = true; 142 | break; 143 | } 144 | case "checked-in": 145 | case "version-controlled-configuration": 146 | { 147 | reader.Skip(); 148 | break; 149 | } 150 | } 151 | } 152 | else if (reader.NodeType == XmlNodeType.EndElement && 153 | string.Equals(reader.LocalName, "response", StringComparison.OrdinalIgnoreCase)) 154 | { 155 | // Remove trailing / if the item is not a collection 156 | var href = itemInfo.Href.TrimEnd('/'); 157 | if (!itemInfo.IsCollection) 158 | { 159 | itemInfo.Href = href; 160 | } 161 | if (string.IsNullOrEmpty(itemInfo.DisplayName) ) 162 | { 163 | var name = href.Substring(href.LastIndexOf('/') + 1); 164 | itemInfo.DisplayName = WebUtility.UrlDecode(name); 165 | } 166 | items.Add(itemInfo); 167 | } 168 | } 169 | } 170 | 171 | return items; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Microsoft 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 | 23 | 24 | 25 | Apache License 26 | 27 | Version 2.0, January 2004 28 | 29 | http://www.apache.org/licenses/ 30 | 31 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 32 | 33 | 1. Definitions. 34 | 35 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 36 | 37 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 38 | 39 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 40 | 41 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 42 | 43 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 44 | 45 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 46 | 47 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 48 | 49 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 52 | 53 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 54 | 55 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 56 | 57 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 58 | 59 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 60 | 61 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 62 | 63 | You must cause any modified files to carry prominent notices stating that You changed the files; and 64 | 65 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 66 | 67 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 68 | 69 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 70 | 71 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 72 | 73 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 74 | 75 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 76 | 77 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # WebDAV Client Roadmap 2 | 3 | This document outlines the recommended improvements and missing features for the WebDAV Client library to enhance its functionality, performance, and usability. 4 | 5 | ## 🎯 Major Missing Features 6 | 7 | ### 1. Advanced Authentication Support 8 | **Priority: High** 9 | **Target: v3.0.0** 10 | 11 | The current implementation only supports basic authentication and Windows authentication. We need to add support for modern authentication methods: 12 | 13 | #### New Authentication Methods 14 | - **Bearer Token Authentication** (OAuth 2.0/JWT) 15 | - **API Key Authentication** 16 | - **Digest Authentication** 17 | - **Custom Authentication Headers** 18 | 19 | #### Implementation Plan 20 | ```csharp 21 | public enum AuthenticationMethod 22 | { 23 | None, 24 | Basic, 25 | Windows, 26 | Bearer, 27 | ApiKey, 28 | Digest, 29 | Custom 30 | } 31 | 32 | // New properties to add to Client class 33 | public AuthenticationMethod AuthMethod { get; set; } 34 | public string BearerToken { get; set; } 35 | public string ApiKeyHeaderName { get; set; } 36 | public string ApiKeyValue { get; set; } 37 | public Dictionary CustomAuthHeaders { get; set; } 38 | ``` 39 | 40 | ### 2. WebDAV Lock/Unlock Operations 41 | **Priority: High** 42 | **Target: v3.0.0** 43 | 44 | Critical WebDAV operations that are currently missing: 45 | 46 | #### New Methods to Implement 47 | ```csharp 48 | // Lock operations 49 | Task LockFile(string filePath, int timeoutSeconds = 600, CancellationToken cancellationToken = default); 50 | Task LockFolder(string folderPath, int timeoutSeconds = 600, CancellationToken cancellationToken = default); 51 | Task UnlockFile(string filePath, string lockToken, CancellationToken cancellationToken = default); 52 | Task UnlockFolder(string folderPath, string lockToken, CancellationToken cancellationToken = default); 53 | Task RefreshLock(string path, string lockToken, int timeoutSeconds = 600, CancellationToken cancellationToken = default); 54 | Task GetLockInfo(string path, CancellationToken cancellationToken = default); 55 | ``` 56 | 57 | #### New Model Classes 58 | ```csharp 59 | public class LockInfo 60 | { 61 | public string Token { get; set; } 62 | public string Owner { get; set; } 63 | public DateTime ExpirationDate { get; set; } 64 | public string LockType { get; set; } 65 | public string LockScope { get; set; } 66 | } 67 | ``` 68 | 69 | ### 3. WebDAV Properties Management (PROPPATCH) 70 | **Priority: Medium** 71 | **Target: v3.1.0** 72 | 73 | The client can read properties but cannot set or manage custom properties: 74 | 75 | #### New Methods to Implement 76 | ```csharp 77 | Task SetProperty(string path, string propertyName, string propertyValue, string nameSpace = "DAV:", CancellationToken cancellationToken = default); 78 | Task> GetCustomProperties(string path, string[] propertyNames = null, CancellationToken cancellationToken = default); 79 | Task RemoveProperty(string path, string propertyName, string nameSpace = "DAV:", CancellationToken cancellationToken = default); 80 | Task SetMultipleProperties(string path, Dictionary properties, string nameSpace = "DAV:", CancellationToken cancellationToken = default); 81 | ``` 82 | 83 | ### 4. WebDAV Search Support (DASL) 84 | **Priority: Low** 85 | **Target: v3.2.0** 86 | 87 | Add support for WebDAV search capabilities: 88 | 89 | ```csharp 90 | Task> Search(string basePath, string query, SearchScope scope = SearchScope.Subtree, CancellationToken cancellationToken = default); 91 | Task> SearchByProperty(string basePath, string propertyName, string propertyValue, CancellationToken cancellationToken = default); 92 | ``` 93 | 94 | ## 🚀 Performance and Reliability Improvements 95 | 96 | ### 5. Retry Mechanism with Exponential Backoff 97 | **Priority: High** 98 | **Target: v2.3.0** 99 | 100 | Add built-in retry logic for transient failures: 101 | 102 | #### New Configuration Properties 103 | ```csharp 104 | public int MaxRetryAttempts { get; set; } = 3; 105 | public TimeSpan RetryDelay { get; set; } = TimeSpan.FromSeconds(1); 106 | public bool EnableExponentialBackoff { get; set; } = true; 107 | public Func RetryPredicate { get; set; } = DefaultRetryPredicate; 108 | ``` 109 | 110 | #### Implementation Details 111 | - Configurable retry attempts 112 | - Exponential backoff strategy 113 | - Custom retry predicates 114 | - Circuit breaker pattern for persistent failures 115 | 116 | ### 6. Progress Reporting for Large Operations 117 | **Priority: Medium** 118 | **Target: v2.4.0** 119 | 120 | Add progress callbacks for long-running operations: 121 | 122 | #### Enhanced Method Signatures 123 | ```csharp 124 | Task Upload(string remoteFilePath, Stream content, string name, IProgress progress = null, CancellationToken cancellationToken = default); 125 | Task Download(string remoteFilePath, IProgress progress = null, CancellationToken cancellationToken = default); 126 | ``` 127 | 128 | #### New Model Classes 129 | ```csharp 130 | public class ProgressInfo 131 | { 132 | public long BytesTransferred { get; set; } 133 | public long TotalBytes { get; set; } 134 | public double PercentageComplete => TotalBytes > 0 ? (double)BytesTransferred / TotalBytes * 100 : 0; 135 | public TimeSpan ElapsedTime { get; set; } 136 | public TimeSpan EstimatedTimeRemaining { get; set; } 137 | public long TransferRate { get; set; } // bytes per second 138 | } 139 | ``` 140 | 141 | ### 7. Connection Pooling and Keep-Alive Optimization 142 | **Priority: Medium** 143 | **Target: v2.3.0** 144 | 145 | Optimize HTTP connection management: 146 | 147 | - Improve HttpClient configuration for connection reuse 148 | - Add connection pool size configuration 149 | - Implement proper keep-alive settings 150 | - Add connection health monitoring 151 | 152 | ### 8. Memory Usage Optimization 153 | **Priority: Medium** 154 | **Target: v2.4.0** 155 | 156 | Reduce memory allocations and improve garbage collection: 157 | 158 | - Stream-based XML parsing to reduce memory footprint 159 | - Object pooling for frequently created objects 160 | - Lazy loading of properties 161 | - Implement `IAsyncDisposable` where appropriate 162 | 163 | ## 🔍 Error Handling and Observability 164 | 165 | ### 9. Structured Logging Support 166 | **Priority: High** 167 | **Target: v2.3.0** 168 | 169 | Add comprehensive logging infrastructure: 170 | 171 | #### Integration with Microsoft.Extensions.Logging 172 | ```csharp 173 | public Client(ICredentials credential = null, TimeSpan? uploadTimeout = null, 174 | IWebProxy proxy = null, ILogger logger = null) 175 | ``` 176 | 177 | #### Logging Categories 178 | - Request/Response details 179 | - Authentication events 180 | - Error conditions 181 | - Performance metrics 182 | - Connection events 183 | 184 | ### 10. Enhanced Exception Information 185 | **Priority: High** 186 | **Target: v2.3.0** 187 | 188 | Improve exception context and debugging information: 189 | 190 | #### Enhanced WebDAVException 191 | ```csharp 192 | public class WebDAVException : Exception 193 | { 194 | public string OperationType { get; set; } 195 | public string RequestUri { get; set; } 196 | public string RequestMethod { get; set; } 197 | public Dictionary RequestHeaders { get; set; } 198 | public string ResponseContent { get; set; } 199 | public TimeSpan RequestDuration { get; set; } 200 | public string ServerVersion { get; set; } 201 | } 202 | ``` 203 | 204 | ### 11. Health Checking and Diagnostics 205 | **Priority: Medium** 206 | **Target: v3.0.0** 207 | 208 | Add server connectivity and health monitoring: 209 | 210 | ```csharp 211 | Task CheckHealth(CancellationToken cancellationToken = default); 212 | Task GetServerCapabilities(CancellationToken cancellationToken = default); 213 | Task GetConnectionInfo(CancellationToken cancellationToken = default); 214 | ``` 215 | 216 | ## 🎨 API Usability Improvements 217 | 218 | ### 12. Fluent Configuration API 219 | **Priority: Medium** 220 | **Target: v3.0.0** 221 | 222 | Create a more intuitive configuration experience: 223 | 224 | ```csharp 225 | public static class ClientBuilder 226 | { 227 | public static ClientConfiguration Create() => new ClientConfiguration(); 228 | } 229 | 230 | public class ClientConfiguration 231 | { 232 | public ClientConfiguration WithServer(string server); 233 | public ClientConfiguration WithBasePath(string basePath); 234 | public ClientConfiguration WithCredentials(ICredentials credentials); 235 | public ClientConfiguration WithBearerToken(string token); 236 | public ClientConfiguration WithTimeout(TimeSpan timeout); 237 | public ClientConfiguration WithRetryPolicy(int maxAttempts, TimeSpan delay); 238 | public ClientConfiguration WithCustomHeaders(Dictionary headers); 239 | public ClientConfiguration WithLogging(ILogger logger); 240 | public Client Build(); 241 | } 242 | 243 | // Usage 244 | var client = ClientBuilder.Create() 245 | .WithServer("https://dav.example.com") 246 | .WithBasePath("/dav/") 247 | .WithBearerToken("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...") 248 | .WithRetryPolicy(3, TimeSpan.FromSeconds(2)) 249 | .Build(); 250 | ``` 251 | 252 | ### 13. Batch Operations Support 253 | **Priority: Medium** 254 | **Target: v3.1.0** 255 | 256 | Add support for batch operations to improve performance: 257 | 258 | ```csharp 259 | Task> UploadMultiple(Dictionary uploads, string remotePath, IProgress progress = null, CancellationToken cancellationToken = default); 260 | Task> DeleteMultiple(IEnumerable paths, CancellationToken cancellationToken = default); 261 | Task> MoveMultiple(Dictionary sourceToDestination, CancellationToken cancellationToken = default); 262 | Task> CopyMultiple(Dictionary sourceToDestination, CancellationToken cancellationToken = default); 263 | ``` 264 | 265 | #### New Model Classes 266 | ```csharp 267 | public class BatchResult 268 | { 269 | public Dictionary Results { get; set; } 270 | public Dictionary Errors { get; set; } 271 | public bool AllSucceeded => Errors.Count == 0; 272 | public int SuccessCount => Results.Count(r => r.Value != null); 273 | public int FailureCount => Errors.Count; 274 | } 275 | 276 | public class BatchProgress 277 | { 278 | public int Completed { get; set; } 279 | public int Total { get; set; } 280 | public string CurrentItem { get; set; } 281 | public double PercentageComplete => Total > 0 ? (double)Completed / Total * 100 : 0; 282 | } 283 | ``` 284 | 285 | ### 14. Async Enumerable Support 286 | **Priority: Low** 287 | **Target: v3.2.0** 288 | 289 | Add support for `IAsyncEnumerable` for large directory listings: 290 | 291 | ```csharp 292 | IAsyncEnumerable ListAsync(string path = "/", int? depth = 1, CancellationToken cancellationToken = default); 293 | IAsyncEnumerable SearchAsync(string basePath, string query, CancellationToken cancellationToken = default); 294 | ``` 295 | 296 | ## 🛠️ Code Quality and Maintenance 297 | 298 | ### 15. Comprehensive Unit Test Coverage 299 | **Priority: High** 300 | **Target: Ongoing** 301 | 302 | Improve test coverage and quality: 303 | 304 | - Increase unit test coverage to >90% 305 | - Add integration tests with real WebDAV servers 306 | - Add performance benchmarks 307 | - Add property-based testing 308 | - Mock server for consistent testing 309 | 310 | ### 16. Code Organization and Refactoring 311 | **Priority: Medium** 312 | **Target: v3.0.0** 313 | 314 | Improve code structure and maintainability: 315 | 316 | - Split large methods into smaller, focused methods 317 | - Extract interfaces for better testability 318 | - Improve separation of concerns 319 | - Add more constants for magic strings and status codes 320 | - Create templated XML content for different operations 321 | 322 | ### 17. Documentation and Examples 323 | **Priority: High** 324 | **Target: v2.3.0** 325 | 326 | Enhance documentation and provide better examples: 327 | 328 | - Complete XML documentation for all public APIs 329 | - Add comprehensive usage examples 330 | - Create getting started guide 331 | - Add troubleshooting guide 332 | - Document authentication scenarios 333 | - Add performance tuning guide 334 | 335 | ### 18. Nullable Reference Types Support 336 | **Priority: Medium** 337 | **Target: v3.0.0** 338 | 339 | Full support for nullable reference types (.NET 8/9): 340 | 341 | - Enable nullable reference types in project 342 | - Add appropriate nullable annotations 343 | - Update method signatures with proper nullability 344 | - Improve null handling throughout codebase 345 | 346 | ## 🔧 Infrastructure and Tooling 347 | 348 | ### 19. Source Generator for WebDAV Properties 349 | **Priority: Low** 350 | **Target: v3.2.0** 351 | 352 | Create source generators for strongly-typed WebDAV properties: 353 | 354 | ```csharp 355 | [WebDAVProperty("DAV:", "creationdate")] 356 | public DateTime CreationDate { get; set; } 357 | 358 | [WebDAVProperty("DAV:", "getcontentlength")] 359 | public long ContentLength { get; set; } 360 | ``` 361 | 362 | ### 20. Performance Monitoring and Metrics 363 | **Priority: Medium** 364 | **Target: v3.1.0** 365 | 366 | Add built-in performance monitoring: 367 | 368 | ```csharp 369 | public class PerformanceMetrics 370 | { 371 | public TimeSpan RequestDuration { get; set; } 372 | public long BytesTransferred { get; set; } 373 | public int RetryCount { get; set; } 374 | public string OperationType { get; set; } 375 | } 376 | 377 | public event EventHandler OperationCompleted; 378 | ``` 379 | 380 | ## 📅 Release Timeline 381 | 382 | ### Version 2.3.0 (Q2 2024) 383 | - Retry mechanism with exponential backoff 384 | - Structured logging support 385 | - Enhanced exception information 386 | - Improved documentation 387 | 388 | ### Version 2.4.0 (Q3 2024) 389 | - Progress reporting for large operations 390 | - Memory usage optimization 391 | - Connection pooling optimization 392 | 393 | ### Version 3.0.0 (Q4 2024) - Major Release 394 | - Advanced authentication support 395 | - WebDAV Lock/Unlock operations 396 | - Fluent configuration API 397 | - Nullable reference types support 398 | - Breaking changes cleanup 399 | 400 | ### Version 3.1.0 (Q1 2025) 401 | - WebDAV properties management (PROPPATCH) 402 | - Batch operations support 403 | - Health checking and diagnostics 404 | - Performance monitoring 405 | 406 | ### Version 3.2.0 (Q2 2025) 407 | - WebDAV search support (DASL) 408 | - Async enumerable support 409 | - Source generator for properties 410 | 411 | ## 🤝 Contributing 412 | 413 | This roadmap is open for community input and contributions. Priority and timeline may be adjusted based on: 414 | 415 | - Community feedback and feature requests 416 | - Security requirements 417 | - Performance benchmarks 418 | - Compatibility considerations 419 | 420 | For each feature implementation: 421 | 1. Create detailed specification 422 | 2. Implement with comprehensive tests 423 | 3. Update documentation 424 | 4. Maintain backward compatibility where possible 425 | 5. Follow semantic versioning 426 | 427 | ## 📊 Success Metrics 428 | 429 | - **Performance**: 20% improvement in request throughput 430 | - **Reliability**: 99.9% success rate for operations with retry mechanism 431 | - **Usability**: Reduce common usage code by 50% with fluent API 432 | - **Adoption**: Increase in NuGet download rates post-release 433 | - **Community**: Active community contributions and feedback 434 | 435 | --- 436 | 437 | *Last updated: December 2024* 438 | *Next review: March 2025* -------------------------------------------------------------------------------- /WebDAVClient/Client.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.Net.Security; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using WebDAVClient.Helpers; 12 | using WebDAVClient.HttpClient; 13 | using WebDAVClient.Model; 14 | 15 | namespace WebDAVClient 16 | { 17 | public class Client : IClient 18 | { 19 | private static readonly HttpMethod m_propFindMethod = new HttpMethod("PROPFIND"); 20 | private static readonly HttpMethod m_moveMethod = new HttpMethod("MOVE"); 21 | private static readonly HttpMethod m_copyMethod = new HttpMethod("COPY"); 22 | 23 | private static readonly HttpMethod m_mkColMethod = new HttpMethod(WebRequestMethods.Http.MkCol); 24 | 25 | private static readonly string m_assemblyVersion = typeof(IClient).Assembly.GetName().Version.ToString(); 26 | private static readonly ProductInfoHeaderValue s_defaultUserAgent = new ProductInfoHeaderValue("WebDAVClient", m_assemblyVersion); 27 | 28 | private const int c_httpStatusCode_MultiStatus = 207; 29 | 30 | // http://webdav.org/specs/rfc4918.html#METHOD_PROPFIND 31 | private const string c_propFindRequestContent = 32 | "" + 33 | "" + 34 | "" + 35 | //" " + 36 | //" " + 37 | //" " + 38 | //" " + 39 | //" " + 40 | //" " + 41 | //" " + 42 | //" " + 43 | //" " + 44 | //" " + 45 | ""; 46 | private static readonly byte[] s_propFindRequestContentBytes = Encoding.UTF8.GetBytes(c_propFindRequestContent); 47 | 48 | private IHttpClientWrapper m_httpClientWrapper; 49 | private readonly bool m_shouldDispose; 50 | private string m_server; 51 | private string m_basePath = "/"; 52 | private string m_encodedBasePath; 53 | private bool m_disposedValue; 54 | 55 | #region WebDAV connection parameters 56 | 57 | /// 58 | /// Specify the WebDAV hostname (required). 59 | /// 60 | public string Server 61 | { 62 | get { return m_server; } 63 | set 64 | { 65 | value = value.TrimEnd('/'); 66 | m_server = value; 67 | } 68 | } 69 | 70 | /// 71 | /// Specify the path of a WebDAV directory to use as 'root' (default: /) 72 | /// 73 | public string BasePath 74 | { 75 | get { return m_basePath; } 76 | set 77 | { 78 | value = value.Trim('/'); 79 | if (string.IsNullOrEmpty(value)) 80 | m_basePath = "/"; 81 | else 82 | m_basePath = "/" + value + "/"; 83 | } 84 | } 85 | 86 | /// 87 | /// Specify a port to use 88 | /// 89 | public int? Port { get; set; } 90 | 91 | /// 92 | /// Specify the UserAgent (and UserAgent version) string to use in requests 93 | /// 94 | public string UserAgent { get; set; } 95 | 96 | /// 97 | /// Specify the UserAgent (and UserAgent version) string to use in requests 98 | /// 99 | public string UserAgentVersion { get; set; } 100 | 101 | /// 102 | /// Specify additional headers to be sent with every request 103 | /// 104 | public ICollection> CustomHeaders { get; set; } 105 | 106 | /// 107 | /// Specify the certificates validation logic 108 | /// 109 | public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } 110 | #endregion 111 | 112 | public Client(ICredentials credential = null, TimeSpan? uploadTimeout = null, IWebProxy proxy = null) 113 | { 114 | var handler = new HttpClientHandler(); 115 | if (proxy != null && handler.SupportsProxy) 116 | { 117 | handler.Proxy = proxy; 118 | } 119 | if (handler.SupportsAutomaticDecompression) 120 | { 121 | handler.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; 122 | } 123 | if (credential != null) 124 | { 125 | handler.Credentials = credential; 126 | handler.PreAuthenticate = true; 127 | } 128 | else 129 | { 130 | handler.UseDefaultCredentials = true; 131 | } 132 | 133 | var client = new System.Net.Http.HttpClient(handler, disposeHandler: true); 134 | client.DefaultRequestHeaders.ExpectContinue = false; 135 | 136 | System.Net.Http.HttpClient uploadClient = null; 137 | if (uploadTimeout != null) 138 | { 139 | uploadClient = new System.Net.Http.HttpClient(handler, disposeHandler: true); 140 | uploadClient.DefaultRequestHeaders.ExpectContinue = false; 141 | uploadClient.Timeout = uploadTimeout.Value; 142 | } 143 | 144 | m_httpClientWrapper = new HttpClientWrapper(client, uploadClient ?? client); 145 | m_shouldDispose = true; 146 | } 147 | 148 | public Client(System.Net.Http.HttpClient httpClient) 149 | { 150 | m_httpClientWrapper = new HttpClientWrapper(httpClient, httpClient); 151 | } 152 | 153 | public Client(IHttpClientWrapper httpClientWrapper) 154 | { 155 | m_httpClientWrapper = httpClientWrapper; 156 | } 157 | 158 | #region WebDAV operations 159 | 160 | /// 161 | /// List all files present on the server. 162 | /// 163 | /// List only files in this path 164 | /// Recursion depth 165 | /// A list of files (entries without a trailing slash) and directories (entries with a trailing slash) 166 | public async Task> List(string path = "/", int? depth = 1, CancellationToken cancellationToken = default) 167 | { 168 | var listUri = await GetServerUrl(path, true).ConfigureAwait(false); 169 | 170 | // Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4 171 | IDictionary headers = new Dictionary(1 + (CustomHeaders?.Count ?? 0)); 172 | if (depth != null) 173 | { 174 | headers.Add("Depth", depth.ToString()); 175 | } 176 | 177 | if (CustomHeaders != null) 178 | { 179 | foreach (var keyValuePair in CustomHeaders) 180 | { 181 | headers.Add(keyValuePair); 182 | } 183 | } 184 | 185 | HttpResponseMessage response = null; 186 | 187 | try 188 | { 189 | response = await HttpRequest(listUri.Uri, m_propFindMethod, headers, s_propFindRequestContentBytes, cancellationToken: cancellationToken).ConfigureAwait(false); 190 | 191 | if (response.StatusCode != HttpStatusCode.OK && 192 | (int) response.StatusCode != c_httpStatusCode_MultiStatus) 193 | { 194 | throw new WebDAVException((int) response.StatusCode, "Failed retrieving items in folder."); 195 | } 196 | 197 | using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) 198 | { 199 | var items = ResponseParser.ParseItems(stream); 200 | 201 | if (items == null) 202 | { 203 | throw new WebDAVException("Failed deserializing data returned from server."); 204 | } 205 | 206 | var listUrl = listUri.ToString(); 207 | 208 | var result = new List(items.Count); 209 | foreach (var item in items) 210 | { 211 | // If it's not a collection, add it to the result 212 | if (!item.IsCollection) 213 | { 214 | result.Add(item); 215 | } 216 | else 217 | { 218 | // If it's not the requested parent folder, add it to the result 219 | var fullHref = await GetServerUrl(item.Href, true).ConfigureAwait(false); 220 | if (!string.Equals(fullHref.ToString(), listUrl, StringComparison.CurrentCultureIgnoreCase)) 221 | { 222 | result.Add(item); 223 | } 224 | } 225 | } 226 | return result; 227 | } 228 | } 229 | finally 230 | { 231 | response?.Dispose(); 232 | } 233 | } 234 | 235 | /// 236 | /// List all files present on the server. 237 | /// 238 | /// An item representing the retrieved folder 239 | public async Task GetFolder(string path = "/", CancellationToken cancellationToken = default) 240 | { 241 | var listUri = await GetServerUrl(path, true).ConfigureAwait(false); 242 | return await Get(listUri.Uri, cancellationToken).ConfigureAwait(false); 243 | } 244 | 245 | /// 246 | /// List all files present on the server. 247 | /// 248 | /// An item representing the retrieved file 249 | public async Task GetFile(string path = "/", CancellationToken cancellationToken = default) 250 | { 251 | var listUri = await GetServerUrl(path, false).ConfigureAwait(false); 252 | return await Get(listUri.Uri, cancellationToken).ConfigureAwait(false); 253 | } 254 | 255 | /// 256 | /// Download a file from the server 257 | /// 258 | /// Source path and filename of the file on the server 259 | /// A stream with the content of the downloaded file 260 | public Task Download(string remoteFilePath, CancellationToken cancellationToken = default) 261 | { 262 | var headers = new Dictionary(1 + (CustomHeaders?.Count ?? 0)) 263 | { 264 | { "translate", "f" } 265 | }; 266 | if (CustomHeaders != null) 267 | { 268 | foreach (var keyValuePair in CustomHeaders) 269 | { 270 | headers.Add(keyValuePair.Key, keyValuePair.Value); 271 | } 272 | } 273 | return DownloadFile(remoteFilePath, headers, cancellationToken); 274 | } 275 | 276 | /// 277 | /// Download a part of file from the server 278 | /// 279 | /// Source path and filename of the file on the server 280 | /// Start bytes of content 281 | /// End bytes of content 282 | /// A stream with the partial content of the downloaded file 283 | public Task DownloadPartial(string remoteFilePath, long startBytes, long endBytes, CancellationToken cancellationToken = default) 284 | { 285 | var headers = new Dictionary(2 + (CustomHeaders?.Count ?? 0)) 286 | { 287 | { "translate", "f" }, 288 | { "Range", "bytes=" + startBytes + "-" + endBytes } 289 | }; 290 | if (CustomHeaders != null) 291 | { 292 | foreach (var keyValuePair in CustomHeaders) 293 | { 294 | headers.Add(keyValuePair.Key, keyValuePair.Value); 295 | } 296 | } 297 | return DownloadFile(remoteFilePath, headers, cancellationToken); 298 | } 299 | 300 | /// 301 | /// Upload a file to the server 302 | /// 303 | /// Source path and filename of the file on the server 304 | /// 305 | /// 306 | /// True if the file was uploaded successfully. False otherwise 307 | public async Task Upload(string remoteFilePath, Stream content, string name, CancellationToken cancellationToken = default) 308 | { 309 | // Should not have a trailing slash. 310 | var uploadUri = await GetServerUrl(remoteFilePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false); 311 | 312 | IDictionary headers = new Dictionary(CustomHeaders?.Count ?? 0); 313 | if (CustomHeaders != null) 314 | { 315 | foreach (var keyValuePair in CustomHeaders) 316 | { 317 | headers.Add(keyValuePair); 318 | } 319 | } 320 | 321 | HttpResponseMessage response = null; 322 | 323 | try 324 | { 325 | response = await HttpUploadRequest(uploadUri.Uri, HttpMethod.Put, content, headers, cancellationToken: cancellationToken).ConfigureAwait(false); 326 | 327 | if (response.StatusCode != HttpStatusCode.OK && 328 | response.StatusCode != HttpStatusCode.NoContent && 329 | response.StatusCode != HttpStatusCode.Created) 330 | { 331 | throw new WebDAVException((int) response.StatusCode, "Failed uploading file."); 332 | } 333 | 334 | return response.IsSuccessStatusCode; 335 | } 336 | finally 337 | { 338 | response?.Dispose(); 339 | } 340 | } 341 | 342 | /// 343 | /// Partial upload a part of file to the server 344 | /// 345 | /// Source path and filename of the file on the server 346 | /// Partial content to update 347 | /// Name of the file to update 348 | /// Start byte position of the target content 349 | /// End bytes of the target content. Must match the length of plus 350 | /// True if the file part was uploaded successfully. False otherwise 351 | public async Task UploadPartial(string remoteFilePath, Stream content, string name, long startBytes, long endBytes, CancellationToken cancellationToken = default) 352 | { 353 | if (startBytes + content.Length != endBytes) 354 | { 355 | throw new InvalidOperationException("The length of the given content plus the startBytes must match the endBytes."); 356 | } 357 | 358 | // Should not have a trailing slash. 359 | var uploadUri = await GetServerUrl(remoteFilePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false); 360 | 361 | HttpResponseMessage response = null; 362 | 363 | try 364 | { 365 | response = await HttpUploadRequest(uploadUri.Uri, HttpMethod.Put, content, null, startBytes, endBytes, cancellationToken: cancellationToken).ConfigureAwait(false); 366 | 367 | if (response.StatusCode != HttpStatusCode.OK && 368 | response.StatusCode != HttpStatusCode.NoContent && 369 | response.StatusCode != HttpStatusCode.Created) 370 | { 371 | throw new WebDAVException((int)response.StatusCode, "Failed uploading file."); 372 | } 373 | 374 | return response.IsSuccessStatusCode; 375 | } 376 | finally 377 | { 378 | response?.Dispose(); 379 | } 380 | } 381 | 382 | /// 383 | /// Create a directory on the server 384 | /// 385 | /// Destination path of the directory on the server 386 | /// The name of the folder to create 387 | /// True if the folder was created successfully. False otherwise 388 | public async Task CreateDir(string remotePath, string name, CancellationToken cancellationToken = default) 389 | { 390 | // Should not have a trailing slash. 391 | var dirUri = await GetServerUrl(remotePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false); 392 | 393 | IDictionary headers = new Dictionary(CustomHeaders?.Count ?? 0); 394 | if (CustomHeaders != null) 395 | { 396 | foreach (var keyValuePair in CustomHeaders) 397 | { 398 | headers.Add(keyValuePair); 399 | } 400 | } 401 | 402 | HttpResponseMessage response = null; 403 | 404 | try 405 | { 406 | response = await HttpRequest(dirUri.Uri, m_mkColMethod, headers, cancellationToken: cancellationToken).ConfigureAwait(false); 407 | 408 | if (response.StatusCode == HttpStatusCode.Conflict) 409 | throw new WebDAVConflictException((int) response.StatusCode, "Failed creating folder."); 410 | 411 | if (response.StatusCode != HttpStatusCode.OK && 412 | response.StatusCode != HttpStatusCode.NoContent && 413 | response.StatusCode != HttpStatusCode.Created) 414 | { 415 | throw new WebDAVException((int)response.StatusCode, "Failed creating folder."); 416 | } 417 | 418 | return response.IsSuccessStatusCode; 419 | } 420 | finally 421 | { 422 | response?.Dispose(); 423 | } 424 | } 425 | 426 | /// 427 | /// Deletes a folder from the server. 428 | /// 429 | public async Task DeleteFolder(string href, CancellationToken cancellationToken = default) 430 | { 431 | var listUri = await GetServerUrl(href, true).ConfigureAwait(false); 432 | await Delete(listUri.Uri, cancellationToken).ConfigureAwait(false); 433 | } 434 | 435 | /// 436 | /// Deletes a file from the server. 437 | /// 438 | public async Task DeleteFile(string href, CancellationToken cancellationToken = default) 439 | { 440 | var listUri = await GetServerUrl(href, false).ConfigureAwait(false); 441 | await Delete(listUri.Uri, cancellationToken).ConfigureAwait(false); 442 | } 443 | 444 | /// 445 | /// Move a folder on the server 446 | /// 447 | /// Source path of the folder on the server 448 | /// Destination path of the folder on the server 449 | /// True if the folder was moved successfully. False otherwise 450 | public async Task MoveFolder(string srcFolderPath, string dstFolderPath, CancellationToken cancellationToken = default) 451 | { 452 | // Should have a trailing slash. 453 | var srcUri = await GetServerUrl(srcFolderPath, true).ConfigureAwait(false); 454 | var dstUri = await GetServerUrl(dstFolderPath, true).ConfigureAwait(false); 455 | 456 | return await Move(srcUri.Uri, dstUri.Uri, cancellationToken).ConfigureAwait(false); 457 | } 458 | 459 | /// 460 | /// Move a file on the server 461 | /// 462 | /// Source path and filename of the file on the server 463 | /// Destination path and filename of the file on the server 464 | /// True if the file was moved successfully. False otherwise 465 | public async Task MoveFile(string srcFilePath, string dstFilePath, CancellationToken cancellationToken = default) 466 | { 467 | // Should not have a trailing slash. 468 | var srcUri = await GetServerUrl(srcFilePath, false).ConfigureAwait(false); 469 | var dstUri = await GetServerUrl(dstFilePath, false).ConfigureAwait(false); 470 | 471 | return await Move(srcUri.Uri, dstUri.Uri, cancellationToken).ConfigureAwait(false); 472 | } 473 | 474 | /// 475 | /// Copies a folder on the server 476 | /// 477 | /// Source path of the folder on the server 478 | /// Destination path of the folder on the server 479 | /// True if the folder was copied successfully. False otherwise 480 | public async Task CopyFolder(string srcFolderPath, string dstFolderPath, CancellationToken cancellationToken = default) 481 | { 482 | // Should have a trailing slash. 483 | var srcUri = await GetServerUrl(srcFolderPath, true).ConfigureAwait(false); 484 | var dstUri = await GetServerUrl(dstFolderPath, true).ConfigureAwait(false); 485 | 486 | return await Copy(srcUri.Uri, dstUri.Uri, cancellationToken).ConfigureAwait(false); 487 | } 488 | 489 | /// 490 | /// Copies a file on the server 491 | /// 492 | /// Source path and filename of the file on the server 493 | /// Destination path and filename of the file on the server 494 | /// True if the file was copied successfully. False otherwise 495 | public async Task CopyFile(string srcFilePath, string dstFilePath, CancellationToken cancellationToken = default) 496 | { 497 | // Should not have a trailing slash. 498 | var srcUri = await GetServerUrl(srcFilePath, false).ConfigureAwait(false); 499 | var dstUri = await GetServerUrl(dstFilePath, false).ConfigureAwait(false); 500 | 501 | return await Copy(srcUri.Uri, dstUri.Uri, cancellationToken).ConfigureAwait(false); 502 | } 503 | 504 | #endregion 505 | 506 | #region Private methods 507 | private async Task Delete(Uri listUri, CancellationToken cancellationToken = default) 508 | { 509 | IDictionary headers = new Dictionary(CustomHeaders?.Count ?? 0); 510 | if (CustomHeaders != null) 511 | { 512 | foreach (var keyValuePair in CustomHeaders) 513 | { 514 | headers.Add(keyValuePair); 515 | } 516 | } 517 | 518 | var response = await HttpRequest(listUri, HttpMethod.Delete, headers, cancellationToken: cancellationToken).ConfigureAwait(false); 519 | 520 | if (response.StatusCode != HttpStatusCode.OK && 521 | response.StatusCode != HttpStatusCode.NoContent) 522 | { 523 | throw new WebDAVException((int)response.StatusCode, "Failed deleting item."); 524 | } 525 | } 526 | 527 | private async Task Get(Uri listUri, CancellationToken cancellationToken = default) 528 | { 529 | // Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4 530 | IDictionary headers = new Dictionary(1 + (CustomHeaders?.Count ?? 0)) 531 | { 532 | { "Depth", "0" } 533 | }; 534 | 535 | if (CustomHeaders != null) 536 | { 537 | foreach (var keyValuePair in CustomHeaders) 538 | { 539 | headers.Add(keyValuePair); 540 | } 541 | } 542 | 543 | HttpResponseMessage response = null; 544 | 545 | try 546 | { 547 | response = await HttpRequest(listUri, m_propFindMethod, headers, s_propFindRequestContentBytes, cancellationToken: cancellationToken).ConfigureAwait(false); 548 | 549 | if (response.StatusCode != HttpStatusCode.OK && 550 | (int) response.StatusCode != c_httpStatusCode_MultiStatus) 551 | { 552 | throw new WebDAVException((int)response.StatusCode, string.Format("Failed retrieving item/folder (Status Code: {0})", response.StatusCode)); 553 | } 554 | 555 | using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) 556 | { 557 | var result = ResponseParser.ParseItem(stream); 558 | 559 | if (result == null) 560 | { 561 | throw new WebDAVException("Failed deserializing data returned from server."); 562 | } 563 | 564 | return result; 565 | } 566 | } 567 | finally 568 | { 569 | response?.Dispose(); 570 | } 571 | } 572 | 573 | private async Task Move(Uri srcUri, Uri dstUri, CancellationToken cancellationToken = default) 574 | { 575 | const string requestContent = "MOVE"; 576 | 577 | IDictionary headers = new Dictionary(1 + (CustomHeaders?.Count ?? 0)) 578 | { 579 | { "Destination", dstUri.ToString() } 580 | }; 581 | 582 | if (CustomHeaders != null) 583 | { 584 | foreach (var keyValuePair in CustomHeaders) 585 | { 586 | headers.Add(keyValuePair); 587 | } 588 | } 589 | 590 | var response = await HttpRequest(srcUri, m_moveMethod, headers, Encoding.UTF8.GetBytes(requestContent), cancellationToken: cancellationToken).ConfigureAwait(false); 591 | 592 | if (response.StatusCode != HttpStatusCode.OK && 593 | response.StatusCode != HttpStatusCode.Created) 594 | { 595 | throw new WebDAVException((int)response.StatusCode, "Failed moving file."); 596 | } 597 | 598 | return response.IsSuccessStatusCode; 599 | } 600 | 601 | private async Task Copy(Uri srcUri, Uri dstUri, CancellationToken cancellationToken = default) 602 | { 603 | const string requestContent = "COPY"; 604 | 605 | IDictionary headers = new Dictionary(1 + (CustomHeaders?.Count ?? 0)) 606 | { 607 | { "Destination", dstUri.ToString() } 608 | }; 609 | 610 | if (CustomHeaders != null) 611 | { 612 | foreach (var keyValuePair in CustomHeaders) 613 | { 614 | headers.Add(keyValuePair); 615 | } 616 | } 617 | 618 | var response = await HttpRequest(srcUri, m_copyMethod, headers, Encoding.UTF8.GetBytes(requestContent), cancellationToken: cancellationToken).ConfigureAwait(false); 619 | 620 | if (response.StatusCode != HttpStatusCode.OK && 621 | response.StatusCode != HttpStatusCode.Created) 622 | { 623 | throw new WebDAVException((int)response.StatusCode, "Failed copying file."); 624 | } 625 | 626 | return response.IsSuccessStatusCode; 627 | } 628 | 629 | private async Task DownloadFile(string remoteFilePath, Dictionary header, CancellationToken cancellationToken = default) 630 | { 631 | // Should not have a trailing slash. 632 | var downloadUri = await GetServerUrl(remoteFilePath, false).ConfigureAwait(false); 633 | var response = await HttpRequest(downloadUri.Uri, HttpMethod.Get, header, cancellationToken: cancellationToken).ConfigureAwait(false); 634 | if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.PartialContent) 635 | { 636 | return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); 637 | } 638 | throw new WebDAVException((int)response.StatusCode, "Failed retrieving file."); 639 | } 640 | 641 | #region Server communication 642 | 643 | /// 644 | /// Perform the WebDAV call and fire the callback when finished. 645 | /// 646 | /// 647 | /// 648 | /// 649 | /// 650 | private async Task HttpRequest(Uri uri, HttpMethod method, IDictionary headers = null, byte[] content = null, CancellationToken cancellationToken = default) 651 | { 652 | using (var request = new HttpRequestMessage(method, uri)) 653 | { 654 | request.Headers.Connection.Add("Keep-Alive"); 655 | if (!string.IsNullOrWhiteSpace(UserAgent)) 656 | request.Headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent, UserAgentVersion)); 657 | else 658 | request.Headers.UserAgent.Add(s_defaultUserAgent); 659 | 660 | if (headers != null) 661 | { 662 | foreach (string key in headers.Keys) 663 | { 664 | request.Headers.Add(key, headers[key]); 665 | } 666 | } 667 | 668 | // Need to send along content? 669 | if (content != null) 670 | { 671 | request.Content = new ByteArrayContent(content); 672 | request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml"); 673 | } 674 | 675 | return await m_httpClientWrapper.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); 676 | } 677 | } 678 | 679 | /// 680 | /// Perform the WebDAV call and fire the callback when finished. 681 | /// 682 | /// 683 | /// 684 | /// 685 | /// 686 | /// 687 | /// 688 | private async Task HttpUploadRequest(Uri uri, HttpMethod method, Stream content, IDictionary headers = null, long? startBytes = null, long? endBytes = null, CancellationToken cancellationToken = default) 689 | { 690 | using (var request = new HttpRequestMessage(method, uri)) 691 | { 692 | request.Headers.Connection.Add("Keep-Alive"); 693 | if (!string.IsNullOrWhiteSpace(UserAgent)) 694 | request.Headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent, UserAgentVersion)); 695 | else 696 | request.Headers.UserAgent.Add(s_defaultUserAgent); 697 | 698 | if (headers != null) 699 | { 700 | foreach (string key in headers.Keys) 701 | { 702 | request.Headers.Add(key, headers[key]); 703 | } 704 | } 705 | 706 | // Need to send along content? 707 | if (content != null) 708 | { 709 | request.Content = new StreamContent(content); 710 | if (startBytes.HasValue && endBytes.HasValue) 711 | { 712 | request.Content.Headers.ContentRange = ContentRangeHeaderValue.Parse($"bytes {startBytes}-{endBytes}/*"); 713 | request.Content.Headers.ContentLength = endBytes - startBytes; 714 | } 715 | } 716 | 717 | return await m_httpClientWrapper.SendUploadAsync(request, cancellationToken).ConfigureAwait(false); 718 | } 719 | } 720 | 721 | /// 722 | /// Try to create an Uri with kind UriKind.Absolute 723 | /// This particular implementation also works on Mono/Linux 724 | /// It seems that on Mono it is expected behavior that URIs 725 | /// of kind /a/b are indeed absolute URIs since it refers to a file in /a/b. 726 | /// https://bugzilla.xamarin.com/show_bug.cgi?id=30854 727 | /// 728 | /// 729 | /// 730 | private static bool TryCreateAbsolute(string uriString, out Uri uriResult) 731 | { 732 | return Uri.TryCreate(uriString, UriKind.Absolute, out uriResult) && uriResult.Scheme != Uri.UriSchemeFile; 733 | } 734 | 735 | private async Task GetServerUrl(string path, bool appendTrailingSlash) 736 | { 737 | // Resolve the base path on the server 738 | if (m_encodedBasePath == null) 739 | { 740 | var baseUri = new UriBuilder(m_server) 741 | { 742 | Path = m_basePath 743 | }; 744 | if (Port != null) 745 | { 746 | baseUri.Port = (int)Port; 747 | } 748 | var root = await Get(baseUri.Uri).ConfigureAwait(false); 749 | 750 | m_encodedBasePath = root.Href; 751 | } 752 | 753 | // If we've been asked for the "root" folder 754 | if (string.IsNullOrEmpty(path)) 755 | { 756 | // If the resolved base path is an absolute URI, use it 757 | if (TryCreateAbsolute(m_encodedBasePath, out Uri absoluteBaseUri)) 758 | { 759 | return new UriBuilder(absoluteBaseUri); 760 | } 761 | 762 | // Otherwise, use the resolved base path relatively to the server 763 | var baseUri = new UriBuilder(m_server) 764 | { 765 | Path = m_encodedBasePath 766 | }; 767 | if (Port != null) 768 | { 769 | baseUri.Port = (int)Port; 770 | } 771 | return baseUri; 772 | } 773 | 774 | // If the requested path is absolute, use it 775 | if (TryCreateAbsolute(path, out Uri absoluteUri)) 776 | { 777 | return new UriBuilder(absoluteUri); 778 | } 779 | else 780 | { 781 | // Otherwise, create a URI relative to the server 782 | UriBuilder baseUri; 783 | if (TryCreateAbsolute(m_encodedBasePath, out absoluteUri)) 784 | { 785 | baseUri = new UriBuilder(absoluteUri); 786 | 787 | baseUri.Path = baseUri.Path.TrimEnd('/') + "/" + path.TrimStart('/'); 788 | 789 | if (appendTrailingSlash && !baseUri.Path.EndsWith("/")) 790 | baseUri.Path += "/"; 791 | } 792 | else 793 | { 794 | baseUri = new UriBuilder(m_server); 795 | if (Port!= null) 796 | { 797 | baseUri.Port = (int)Port; 798 | } 799 | 800 | // Ensure we don't add the base path twice 801 | var finalPath = path; 802 | if (!finalPath.StartsWith(m_encodedBasePath, StringComparison.InvariantCultureIgnoreCase)) 803 | { 804 | finalPath = m_encodedBasePath.TrimEnd('/') + "/" + path; 805 | } 806 | if (appendTrailingSlash) 807 | finalPath = finalPath.TrimEnd('/') + "/"; 808 | 809 | baseUri.Path = finalPath; 810 | } 811 | 812 | return baseUri; 813 | } 814 | } 815 | 816 | #endregion 817 | 818 | #endregion 819 | 820 | #region WebDAV Connection Helpers 821 | 822 | public bool ServerCertificateValidation(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certification, System.Security.Cryptography.X509Certificates.X509Chain chain, SslPolicyErrors sslPolicyErrors) 823 | { 824 | if (ServerCertificateValidationCallback != null) 825 | { 826 | return ServerCertificateValidationCallback(sender, certification, chain, sslPolicyErrors); 827 | } 828 | return false; 829 | } 830 | #endregion 831 | 832 | #region IDisposable methods 833 | protected virtual void Dispose(bool disposing) 834 | { 835 | if (!m_disposedValue) 836 | { 837 | if (disposing) 838 | { 839 | if (m_shouldDispose) 840 | { 841 | if (m_httpClientWrapper is IDisposable httpClientWrapperDisposable) 842 | { 843 | httpClientWrapperDisposable.Dispose(); 844 | m_httpClientWrapper = null; 845 | } 846 | } 847 | } 848 | 849 | m_disposedValue = true; 850 | } 851 | } 852 | 853 | public void Dispose() 854 | { 855 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 856 | Dispose(disposing: true); 857 | GC.SuppressFinalize(this); 858 | } 859 | #endregion 860 | } 861 | } 862 | --------------------------------------------------------------------------------