├── .gitignore
├── LICENSE
├── README.md
├── TusDotNetClient.sln
├── src
└── TusDotNetClient
│ ├── TusClient.cs
│ ├── TusDotNetClient.csproj
│ ├── TusException.cs
│ ├── TusHTTPClient.cs
│ ├── TusHeaderNames.cs
│ ├── TusHttpRequest.cs
│ ├── TusHttpResponse.cs
│ ├── TusOperation.cs
│ └── TusServerInfo.cs
└── tests
└── TusDotNetClientTests
├── Fixture.cs
├── TusClientTests.cs
├── TusDotNetClientTests.csproj
└── Utils.cs
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/node,rider,linux,macos,windows,monodevelop,visualstudio,jetbrains+all,visualstudiocode
3 | # Edit at https://www.gitignore.io/?templates=node,rider,linux,macos,windows,monodevelop,visualstudio,jetbrains+all,visualstudiocode
4 |
5 | ### JetBrains+all ###
6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
8 |
9 | # User-specific stuff
10 | .idea/**/workspace.xml
11 | .idea/**/tasks.xml
12 | .idea/**/usage.statistics.xml
13 | .idea/**/dictionaries
14 | .idea/**/shelf
15 |
16 | # Generated files
17 | .idea/**/contentModel.xml
18 |
19 | # Sensitive or high-churn files
20 | .idea/**/dataSources/
21 | .idea/**/dataSources.ids
22 | .idea/**/dataSources.local.xml
23 | .idea/**/sqlDataSources.xml
24 | .idea/**/dynamic.xml
25 | .idea/**/uiDesigner.xml
26 | .idea/**/dbnavigator.xml
27 |
28 | # Gradle
29 | .idea/**/gradle.xml
30 | .idea/**/libraries
31 |
32 | # Gradle and Maven with auto-import
33 | # When using Gradle or Maven with auto-import, you should exclude module files,
34 | # since they will be recreated, and may cause churn. Uncomment if using
35 | # auto-import.
36 | # .idea/modules.xml
37 | # .idea/*.iml
38 | # .idea/modules
39 |
40 | # CMake
41 | cmake-build-*/
42 |
43 | # Mongo Explorer plugin
44 | .idea/**/mongoSettings.xml
45 |
46 | # File-based project format
47 | *.iws
48 |
49 | # IntelliJ
50 | out/
51 |
52 | # mpeltonen/sbt-idea plugin
53 | .idea_modules/
54 |
55 | # JIRA plugin
56 | atlassian-ide-plugin.xml
57 |
58 | # Cursive Clojure plugin
59 | .idea/replstate.xml
60 |
61 | # Crashlytics plugin (for Android Studio and IntelliJ)
62 | com_crashlytics_export_strings.xml
63 | crashlytics.properties
64 | crashlytics-build.properties
65 | fabric.properties
66 |
67 | # Editor-based Rest Client
68 | .idea/httpRequests
69 |
70 | # Android studio 3.1+ serialized cache file
71 | .idea/caches/build_file_checksums.ser
72 |
73 | ### JetBrains+all Patch ###
74 | # Ignores the whole .idea folder and all .iml files
75 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
76 |
77 | .idea/
78 |
79 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
80 |
81 | *.iml
82 | modules.xml
83 | .idea/misc.xml
84 | *.ipr
85 |
86 | ### Linux ###
87 | *~
88 |
89 | # temporary files which can be created if a process still has a handle open of a deleted file
90 | .fuse_hidden*
91 |
92 | # KDE directory preferences
93 | .directory
94 |
95 | # Linux trash folder which might appear on any partition or disk
96 | .Trash-*
97 |
98 | # .nfs files are created when an open file is removed but is still being accessed
99 | .nfs*
100 |
101 | ### macOS ###
102 | # General
103 | .DS_Store
104 | .AppleDouble
105 | .LSOverride
106 |
107 | # Icon must end with two \r
108 | Icon
109 |
110 | # Thumbnails
111 | ._*
112 |
113 | # Files that might appear in the root of a volume
114 | .DocumentRevisions-V100
115 | .fseventsd
116 | .Spotlight-V100
117 | .TemporaryItems
118 | .Trashes
119 | .VolumeIcon.icns
120 | .com.apple.timemachine.donotpresent
121 |
122 | # Directories potentially created on remote AFP share
123 | .AppleDB
124 | .AppleDesktop
125 | Network Trash Folder
126 | Temporary Items
127 | .apdisk
128 |
129 | ### MonoDevelop ###
130 | #User Specific
131 | *.userprefs
132 | *.usertasks
133 |
134 | #Mono Project Files
135 | *.pidb
136 | *.resources
137 | test-results/
138 |
139 | ### Node ###
140 | # Logs
141 | logs
142 | *.log
143 | npm-debug.log*
144 | yarn-debug.log*
145 | yarn-error.log*
146 |
147 | # Runtime data
148 | pids
149 | *.pid
150 | *.seed
151 | *.pid.lock
152 |
153 | # Directory for instrumented libs generated by jscoverage/JSCover
154 | lib-cov
155 |
156 | # Coverage directory used by tools like istanbul
157 | coverage
158 |
159 | # nyc test coverage
160 | .nyc_output
161 |
162 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
163 | .grunt
164 |
165 | # Bower dependency directory (https://bower.io/)
166 | bower_components
167 |
168 | # node-waf configuration
169 | .lock-wscript
170 |
171 | # Compiled binary addons (https://nodejs.org/api/addons.html)
172 | build/Release
173 |
174 | # Dependency directories
175 | node_modules/
176 | jspm_packages/
177 |
178 | # TypeScript v1 declaration files
179 | typings/
180 |
181 | # Optional npm cache directory
182 | .npm
183 |
184 | # Optional eslint cache
185 | .eslintcache
186 |
187 | # Optional REPL history
188 | .node_repl_history
189 |
190 | # Output of 'npm pack'
191 | *.tgz
192 |
193 | # Yarn Integrity file
194 | .yarn-integrity
195 |
196 | # dotenv environment variables file
197 | .env
198 |
199 | # parcel-bundler cache (https://parceljs.org/)
200 | .cache
201 |
202 | # next.js build output
203 | .next
204 |
205 | # nuxt.js build output
206 | .nuxt
207 |
208 | # vuepress build output
209 | .vuepress/dist
210 |
211 | # Serverless directories
212 | .serverless/
213 |
214 | # FuseBox cache
215 | .fusebox/
216 |
217 | #DynamoDB Local files
218 | .dynamodb/
219 |
220 | ### Rider ###
221 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
222 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
223 |
224 | # User-specific stuff
225 |
226 | # Generated files
227 |
228 | # Sensitive or high-churn files
229 |
230 | # Gradle
231 |
232 | # Gradle and Maven with auto-import
233 | # When using Gradle or Maven with auto-import, you should exclude module files,
234 | # since they will be recreated, and may cause churn. Uncomment if using
235 | # auto-import.
236 | # .idea/modules.xml
237 | # .idea/*.iml
238 | # .idea/modules
239 |
240 | # CMake
241 |
242 | # Mongo Explorer plugin
243 |
244 | # File-based project format
245 |
246 | # IntelliJ
247 |
248 | # mpeltonen/sbt-idea plugin
249 |
250 | # JIRA plugin
251 |
252 | # Cursive Clojure plugin
253 |
254 | # Crashlytics plugin (for Android Studio and IntelliJ)
255 |
256 | # Editor-based Rest Client
257 |
258 | # Android studio 3.1+ serialized cache file
259 |
260 | ### VisualStudioCode ###
261 | .vscode/*
262 | !.vscode/settings.json
263 | !.vscode/tasks.json
264 | !.vscode/launch.json
265 | !.vscode/extensions.json
266 |
267 | ### VisualStudioCode Patch ###
268 | # Ignore all local history of files
269 | .history
270 |
271 | ### Windows ###
272 | # Windows thumbnail cache files
273 | Thumbs.db
274 | ehthumbs.db
275 | ehthumbs_vista.db
276 |
277 | # Dump file
278 | *.stackdump
279 |
280 | # Folder config file
281 | [Dd]esktop.ini
282 |
283 | # Recycle Bin used on file shares
284 | $RECYCLE.BIN/
285 |
286 | # Windows Installer files
287 | *.cab
288 | *.msi
289 | *.msix
290 | *.msm
291 | *.msp
292 |
293 | # Windows shortcuts
294 | *.lnk
295 |
296 | ### VisualStudio ###
297 | ## Ignore Visual Studio temporary files, build results, and
298 | ## files generated by popular Visual Studio add-ons.
299 | ##
300 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
301 |
302 | # User-specific files
303 | *.rsuser
304 | *.suo
305 | *.user
306 | *.userosscache
307 | *.sln.docstates
308 |
309 | # User-specific files (MonoDevelop/Xamarin Studio)
310 |
311 | # Build results
312 | [Dd]ebug/
313 | [Dd]ebugPublic/
314 | [Rr]elease/
315 | [Rr]eleases/
316 | x64/
317 | x86/
318 | [Aa][Rr][Mm]/
319 | [Aa][Rr][Mm]64/
320 | bld/
321 | [Bb]in/
322 | [Oo]bj/
323 | [Ll]og/
324 |
325 | # Visual Studio 2015/2017 cache/options directory
326 | .vs/
327 | # Uncomment if you have tasks that create the project's static files in wwwroot
328 | #wwwroot/
329 |
330 | # Visual Studio 2017 auto generated files
331 | Generated\ Files/
332 |
333 | # MSTest test Results
334 | [Tt]est[Rr]esult*/
335 | [Bb]uild[Ll]og.*
336 |
337 | # NUNIT
338 | *.VisualState.xml
339 | TestResult.xml
340 |
341 | # Build Results of an ATL Project
342 | [Dd]ebugPS/
343 | [Rr]eleasePS/
344 | dlldata.c
345 |
346 | # Benchmark Results
347 | BenchmarkDotNet.Artifacts/
348 |
349 | # .NET Core
350 | project.lock.json
351 | project.fragment.lock.json
352 | artifacts/
353 |
354 | # StyleCop
355 | StyleCopReport.xml
356 |
357 | # Files built by Visual Studio
358 | *_i.c
359 | *_p.c
360 | *_h.h
361 | *.ilk
362 | *.meta
363 | *.obj
364 | *.iobj
365 | *.pch
366 | *.pdb
367 | *.ipdb
368 | *.pgc
369 | *.pgd
370 | *.rsp
371 | *.sbr
372 | *.tlb
373 | *.tli
374 | *.tlh
375 | *.tmp
376 | *.tmp_proj
377 | *_wpftmp.csproj
378 | *.vspscc
379 | *.vssscc
380 | .builds
381 | *.svclog
382 | *.scc
383 |
384 | # Chutzpah Test files
385 | _Chutzpah*
386 |
387 | # Visual C++ cache files
388 | ipch/
389 | *.aps
390 | *.ncb
391 | *.opendb
392 | *.opensdf
393 | *.sdf
394 | *.cachefile
395 | *.VC.db
396 | *.VC.VC.opendb
397 |
398 | # Visual Studio profiler
399 | *.psess
400 | *.vsp
401 | *.vspx
402 | *.sap
403 |
404 | # Visual Studio Trace Files
405 | *.e2e
406 |
407 | # TFS 2012 Local Workspace
408 | $tf/
409 |
410 | # Guidance Automation Toolkit
411 | *.gpState
412 |
413 | # ReSharper is a .NET coding add-in
414 | _ReSharper*/
415 | *.[Rr]e[Ss]harper
416 | *.DotSettings.user
417 |
418 | # JustCode is a .NET coding add-in
419 | .JustCode
420 |
421 | # TeamCity is a build add-in
422 | _TeamCity*
423 |
424 | # DotCover is a Code Coverage Tool
425 | *.dotCover
426 |
427 | # AxoCover is a Code Coverage Tool
428 | .axoCover/*
429 | !.axoCover/settings.json
430 |
431 | # Visual Studio code coverage results
432 | *.coverage
433 | *.coveragexml
434 |
435 | # NCrunch
436 | _NCrunch_*
437 | .*crunch*.local.xml
438 | nCrunchTemp_*
439 |
440 | # MightyMoose
441 | *.mm.*
442 | AutoTest.Net/
443 |
444 | # Web workbench (sass)
445 | .sass-cache/
446 |
447 | # Installshield output folder
448 | [Ee]xpress/
449 |
450 | # DocProject is a documentation generator add-in
451 | DocProject/buildhelp/
452 | DocProject/Help/*.HxT
453 | DocProject/Help/*.HxC
454 | DocProject/Help/*.hhc
455 | DocProject/Help/*.hhk
456 | DocProject/Help/*.hhp
457 | DocProject/Help/Html2
458 | DocProject/Help/html
459 |
460 | # Click-Once directory
461 | publish/
462 |
463 | # Publish Web Output
464 | *.[Pp]ublish.xml
465 | *.azurePubxml
466 | # Note: Comment the next line if you want to checkin your web deploy settings,
467 | # but database connection strings (with potential passwords) will be unencrypted
468 | *.pubxml
469 | *.publishproj
470 |
471 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
472 | # checkin your Azure Web App publish settings, but sensitive information contained
473 | # in these scripts will be unencrypted
474 | PublishScripts/
475 |
476 | # NuGet Packages
477 | *.nupkg
478 | # The packages folder can be ignored because of Package Restore
479 | **/[Pp]ackages/*
480 | # except build/, which is used as an MSBuild target.
481 | !**/[Pp]ackages/build/
482 | # Uncomment if necessary however generally it will be regenerated when needed
483 | #!**/[Pp]ackages/repositories.config
484 | # NuGet v3's project.json files produces more ignorable files
485 | *.nuget.props
486 | *.nuget.targets
487 |
488 | # Microsoft Azure Build Output
489 | csx/
490 | *.build.csdef
491 |
492 | # Microsoft Azure Emulator
493 | ecf/
494 | rcf/
495 |
496 | # Windows Store app package directories and files
497 | AppPackages/
498 | BundleArtifacts/
499 | Package.StoreAssociation.xml
500 | _pkginfo.txt
501 | *.appx
502 |
503 | # Visual Studio cache files
504 | # files ending in .cache can be ignored
505 | *.[Cc]ache
506 | # but keep track of directories ending in .cache
507 | !*.[Cc]ache/
508 |
509 | # Others
510 | ClientBin/
511 | ~$*
512 | *.dbmdl
513 | *.dbproj.schemaview
514 | *.jfm
515 | *.pfx
516 | *.publishsettings
517 | orleans.codegen.cs
518 |
519 | # Including strong name files can present a security risk
520 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
521 | #*.snk
522 |
523 | # Since there are multiple workflows, uncomment next line to ignore bower_components
524 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
525 | #bower_components/
526 | # ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true
527 | **/wwwroot/lib/
528 |
529 | # RIA/Silverlight projects
530 | Generated_Code/
531 |
532 | # Backup & report files from converting an old project file
533 | # to a newer Visual Studio version. Backup files are not needed,
534 | # because we have git ;-)
535 | _UpgradeReport_Files/
536 | Backup*/
537 | UpgradeLog*.XML
538 | UpgradeLog*.htm
539 | ServiceFabricBackup/
540 | *.rptproj.bak
541 |
542 | # SQL Server files
543 | *.mdf
544 | *.ldf
545 | *.ndf
546 |
547 | # Business Intelligence projects
548 | *.rdl.data
549 | *.bim.layout
550 | *.bim_*.settings
551 | *.rptproj.rsuser
552 |
553 | # Microsoft Fakes
554 | FakesAssemblies/
555 |
556 | # GhostDoc plugin setting file
557 | *.GhostDoc.xml
558 |
559 | # Node.js Tools for Visual Studio
560 | .ntvs_analysis.dat
561 |
562 | # Visual Studio 6 build log
563 | *.plg
564 |
565 | # Visual Studio 6 workspace options file
566 | *.opt
567 |
568 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
569 | *.vbw
570 |
571 | # Visual Studio LightSwitch build output
572 | **/*.HTMLClient/GeneratedArtifacts
573 | **/*.DesktopClient/GeneratedArtifacts
574 | **/*.DesktopClient/ModelManifest.xml
575 | **/*.Server/GeneratedArtifacts
576 | **/*.Server/ModelManifest.xml
577 | _Pvt_Extensions
578 |
579 | # Paket dependency manager
580 | .paket/paket.exe
581 | paket-files/
582 |
583 | # FAKE - F# Make
584 | .fake/
585 |
586 | # JetBrains Rider
587 | *.sln.iml
588 |
589 | # CodeRush personal settings
590 | .cr/personal
591 |
592 | # Python Tools for Visual Studio (PTVS)
593 | __pycache__/
594 | *.pyc
595 |
596 | # Cake - Uncomment if you are using it
597 | # tools/**
598 | # !tools/packages.config
599 |
600 | # Tabs Studio
601 | *.tss
602 |
603 | # Telerik's JustMock configuration file
604 | *.jmconfig
605 |
606 | # BizTalk build output
607 | *.btp.cs
608 | *.btm.cs
609 | *.odx.cs
610 | *.xsd.cs
611 |
612 | # OpenCover UI analysis results
613 | OpenCover/
614 |
615 | # Azure Stream Analytics local run output
616 | ASALocalRun/
617 |
618 | # MSBuild Binary and Structured Log
619 | *.binlog
620 |
621 | # NVidia Nsight GPU debugger configuration file
622 | *.nvuser
623 |
624 | # MFractors (Xamarin productivity tool) working folder
625 | .mfractor/
626 |
627 | # Local History for Visual Studio
628 | .localhistory/
629 |
630 | # End of https://www.gitignore.io/api/node,rider,linux,macos,windows,monodevelop,visualstudio,jetbrains+all,visualstudiocode
631 |
632 | tusd
633 | tusd.exe
634 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Tus Client for .Net Contributors
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TusDotNetClient
2 | .Net client for [tus.io](http://tus.io/) Resumable File Upload protocol.
3 |
4 | ## Features
5 | - Supports tus v1.0.0
6 | - Protocol extensions supported: Creation, Termination
7 | - Upload progress events
8 |
9 | ## Usage
10 | ```c#
11 | var file = new FileInfo(@"path/to/file.ext");
12 | var client = new TusClient();
13 | var fileUrl = await client.CreateAsync(Address, file.Length, metadata);
14 | await client.UploadAsync(fileUrl, file, chunkSize: 5D);
15 | ```
16 |
17 | ### Progress updates
18 | `UploadAsync` returns an object of type `TusOperation`, which exposes an event which will report the progress of the upload.
19 |
20 | Store the return object in a variable and subscribe to the `Progressed` event for updates. The upload operation will not start until `TusOperation` is `await`ed.
21 |
22 | ```c#
23 | var file = new FileInfo(@"path/to/file.ext");
24 | var client = new TusClient();
25 | var fileUrl = await client.CreateAsync(Address, file.Length, metadata);
26 | var uploadOperation = client.UploadAsync(fileUrl, file, chunkSize: 5D);
27 |
28 | uploadOperation.Progressed += (transferred, total) =>
29 | System.Diagnostics.Debug.WriteLine($"Progress: {transferred}/{total}");
30 |
31 | await uploadOperation;
32 | ```
33 |
34 | ## License
35 | MIT
36 |
--------------------------------------------------------------------------------
/TusDotNetClient.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TusDotNetClient", "src\TusDotNetClient\TusDotNetClient.csproj", "{B81309A8-D42F-4DC7-A10F-A67D95D46E26}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TusDotNetClientTests", "tests\TusDotNetClientTests\TusDotNetClientTests.csproj", "{AF358546-787A-45C1-95A2-0C6162A25C5E}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7A3A7595-66D1-4690-BF96-521F787946A8}"
11 | ProjectSection(SolutionItems) = preProject
12 | README.md = README.md
13 | EndProjectSection
14 | EndProject
15 | Global
16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
17 | Debug|Any CPU = Debug|Any CPU
18 | Debug|x64 = Debug|x64
19 | Debug|x86 = Debug|x86
20 | Release|Any CPU = Release|Any CPU
21 | Release|x64 = Release|x64
22 | Release|x86 = Release|x86
23 | EndGlobalSection
24 | GlobalSection(SolutionProperties) = preSolution
25 | HideSolutionNode = FALSE
26 | EndGlobalSection
27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
28 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Debug|x64.ActiveCfg = Debug|Any CPU
31 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Debug|x64.Build.0 = Debug|Any CPU
32 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Debug|x86.ActiveCfg = Debug|Any CPU
33 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Debug|x86.Build.0 = Debug|Any CPU
34 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Release|Any CPU.Build.0 = Release|Any CPU
36 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Release|x64.ActiveCfg = Release|Any CPU
37 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Release|x64.Build.0 = Release|Any CPU
38 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Release|x86.ActiveCfg = Release|Any CPU
39 | {B81309A8-D42F-4DC7-A10F-A67D95D46E26}.Release|x86.Build.0 = Release|Any CPU
40 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Debug|x64.ActiveCfg = Debug|Any CPU
43 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Debug|x64.Build.0 = Debug|Any CPU
44 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Debug|x86.ActiveCfg = Debug|Any CPU
45 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Debug|x86.Build.0 = Debug|Any CPU
46 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Release|Any CPU.Build.0 = Release|Any CPU
48 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Release|x64.ActiveCfg = Release|Any CPU
49 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Release|x64.Build.0 = Release|Any CPU
50 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Release|x86.ActiveCfg = Release|Any CPU
51 | {AF358546-787A-45C1-95A2-0C6162A25C5E}.Release|x86.Build.0 = Release|Any CPU
52 | EndGlobalSection
53 | EndGlobal
54 |
--------------------------------------------------------------------------------
/src/TusDotNetClient/TusClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Net.Sockets;
7 | using System.Security.Cryptography;
8 | using System.Text;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 |
12 | namespace TusDotNetClient
13 | {
14 | ///
15 | /// Represents the different hashing algorithm implementations supported by
16 | ///
17 | public enum HashingImplementation
18 | {
19 | Sha1Managed,
20 | SHA1CryptoServiceProvider,
21 | }
22 |
23 | ///
24 | /// A class to perform actions against a Tus enabled server.
25 | ///
26 | public class TusClient
27 | {
28 | ///
29 | /// Get or set the hashing algorithm implementation to be used for checksum calculation.
30 | ///
31 | public static HashingImplementation HashingImplementation { get; set; } =
32 | HashingImplementation.Sha1Managed;
33 |
34 | ///
35 | /// A mutable dictionary of headers which will be included with all requests.
36 | ///
37 | public Dictionary AdditionalHeaders { get; } =
38 | new Dictionary(StringComparer.OrdinalIgnoreCase);
39 |
40 | ///
41 | /// Get or set the proxy to use when making requests.
42 | ///
43 | public IWebProxy Proxy { get; set; }
44 |
45 | ///
46 | /// Create a file at the Tus server.
47 | ///
48 | /// URL to the creation endpoint of the Tus server.
49 | /// The file which will be uploaded.
50 | /// Metadata to be stored alongside the file.
51 | /// The URL to the created file.
52 | /// Throws if the response doesn't contain the required information.
53 | public async Task CreateAsync(string url, FileInfo fileInfo,
54 | params (string key, string value)[] metadata)
55 | {
56 | if (!metadata.Any(m => m.key == "filename"))
57 | {
58 | metadata = metadata.Concat(new[] {("filename", fileInfo.Name)}).ToArray();
59 | }
60 |
61 | return await CreateAsync(url, fileInfo.Length, metadata);
62 | }
63 |
64 | ///
65 | /// Create a file at the Tus server.
66 | ///
67 | /// URL to the creation endpoint of the Tus server.
68 | /// The byte size of the file which will be uploaded.
69 | /// Metadata to be stored alongside the file.
70 | /// The URL to the created file.
71 | /// Throws if the response doesn't contain the required information.
72 | public async Task CreateAsync(string url, long uploadLength,
73 | params (string key, string value)[] metadata)
74 | {
75 | var requestUri = new Uri(url);
76 | var client = new TusHttpClient
77 | {
78 | Proxy = Proxy
79 | };
80 |
81 | var request = new TusHttpRequest(url, RequestMethod.Post, AdditionalHeaders);
82 |
83 | request.AddHeader(TusHeaderNames.UploadLength, uploadLength.ToString());
84 | request.AddHeader(TusHeaderNames.ContentLength, "0");
85 |
86 | if (metadata.Length > 0)
87 | {
88 | request.AddHeader(TusHeaderNames.UploadMetadata, string.Join(",", metadata
89 | .Select(md =>
90 | $"{md.key.Replace(" ", "").Replace(",", "")} {Convert.ToBase64String(Encoding.UTF8.GetBytes(md.value))}")));
91 | }
92 |
93 | var response = await client.PerformRequestAsync(request)
94 | .ConfigureAwait(false);
95 |
96 | if (response.StatusCode != HttpStatusCode.Created)
97 | throw new Exception("CreateFileInServer failed. " + response.ResponseString);
98 |
99 | if (!response.Headers.ContainsKey("Location"))
100 | throw new Exception("Location Header Missing");
101 |
102 | if (!Uri.TryCreate(response.Headers["Location"], UriKind.RelativeOrAbsolute, out var locationUri))
103 | throw new Exception("Invalid Location Header");
104 |
105 | if (!locationUri.IsAbsoluteUri)
106 | locationUri = new Uri(requestUri, locationUri);
107 |
108 | return locationUri.ToString();
109 | }
110 |
111 | ///
112 | /// Upload a file to the Tus server.
113 | ///
114 | /// URL to a previously created file.
115 | /// The file to upload.
116 | /// The size, in megabytes, of each file chunk when uploading.
117 | /// A cancellation token to cancel the operation with.
118 | /// A which represents the upload operation.
119 | public TusOperation> UploadAsync(
120 | string url,
121 | FileInfo file,
122 | double chunkSize = 5.0,
123 | CancellationToken cancellationToken = default)
124 | {
125 | FileStream fileStream = new FileStream(
126 | file.FullName,
127 | FileMode.Open,
128 | FileAccess.Read,
129 | FileShare.Read,
130 | ChunkSizeToMB(chunkSize),
131 | true);
132 |
133 | return UploadAsync(
134 | url,
135 | fileStream,
136 | chunkSize,
137 | cancellationToken);
138 | }
139 |
140 | ///
141 | /// Upload a file to the Tus server.
142 | ///
143 | /// URL to a previously created file.
144 | /// A file stream of the file to upload. The stream will be closed automatically.
145 | /// The size, in megabytes, of each file chunk when uploading.
146 | /// A cancellation token to cancel the operation with.
147 | /// A which represents the upload operation.
148 | public TusOperation> UploadAsync(
149 | string url,
150 | Stream fileStream,
151 | double chunkSize = 5.0,
152 | CancellationToken cancellationToken = default)
153 | {
154 | return new TusOperation>(
155 | async reportProgress =>
156 | {
157 | try
158 | {
159 | var offset = await GetFileOffset(url)
160 | .ConfigureAwait(false);
161 |
162 | var client = new TusHttpClient();
163 | var sha = HashingImplementation == HashingImplementation.Sha1Managed
164 | ? (SHA1) new SHA1Managed()
165 | : new SHA1CryptoServiceProvider();
166 |
167 | var uploadChunkSize = ChunkSizeToMB(chunkSize);
168 |
169 | if (offset == fileStream.Length)
170 | reportProgress(fileStream.Length, fileStream.Length);
171 |
172 | var buffer = new byte[uploadChunkSize];
173 |
174 | void OnProgress(long written, long total) =>
175 | reportProgress(offset + written, fileStream.Length);
176 |
177 | List responses = new List();
178 |
179 | while (offset < fileStream.Length)
180 | {
181 | fileStream.Seek(offset, SeekOrigin.Begin);
182 |
183 | var bytesRead = await fileStream.ReadAsync(buffer, 0, uploadChunkSize);
184 | var segment = new ArraySegment(buffer, 0, bytesRead);
185 | var sha1Hash = sha.ComputeHash(buffer, 0, bytesRead);
186 |
187 | var request = new TusHttpRequest(url, RequestMethod.Patch, AdditionalHeaders, segment,
188 | cancellationToken);
189 | request.AddHeader(TusHeaderNames.UploadOffset, offset.ToString());
190 | request.AddHeader(TusHeaderNames.UploadChecksum,
191 | $"sha1 {Convert.ToBase64String(sha1Hash)}");
192 | request.AddHeader(TusHeaderNames.ContentType, "application/offset+octet-stream");
193 |
194 | try
195 | {
196 | request.UploadProgressed += OnProgress;
197 | var response = await client.PerformRequestAsync(request)
198 | .ConfigureAwait(false);
199 | responses.Add(response);
200 | request.UploadProgressed -= OnProgress;
201 |
202 | if (response.StatusCode != HttpStatusCode.NoContent)
203 | {
204 | throw new Exception("WriteFileInServer failed. " + response.ResponseString);
205 | }
206 |
207 | offset = long.Parse(response.Headers[TusHeaderNames.UploadOffset]);
208 |
209 | // reportProgress(offset, fileStream.Length);
210 | }
211 | catch (IOException ex)
212 | {
213 | if (ex.InnerException is SocketException socketException)
214 | {
215 | if (socketException.SocketErrorCode == SocketError.ConnectionReset)
216 | {
217 | // retry by continuing the while loop
218 | // but get new offset from server to prevent Conflict error
219 | offset = await GetFileOffset(url)
220 | .ConfigureAwait(false);
221 | }
222 | else
223 | {
224 | throw;
225 | }
226 | }
227 | else
228 | {
229 | throw;
230 | }
231 | }
232 | }
233 |
234 | return responses;
235 | }
236 | finally
237 | {
238 | fileStream.Dispose();
239 | }
240 | });
241 | }
242 |
243 | ///
244 | /// Download a file from the Tus server.
245 | ///
246 | /// The URL of a file at the Tus server.
247 | /// A cancellation token to cancel the operation with.
248 | /// A which represents the download operation.
249 | public TusOperation DownloadAsync(string url, CancellationToken cancellationToken = default) =>
250 | new TusOperation(
251 | async reportProgress =>
252 | {
253 | var client = new TusHttpClient();
254 | var request = new TusHttpRequest(
255 | url,
256 | RequestMethod.Get,
257 | AdditionalHeaders,
258 | cancelToken: cancellationToken);
259 |
260 | request.DownloadProgressed += reportProgress;
261 |
262 | var response = await client.PerformRequestAsync(request)
263 | .ConfigureAwait(false);
264 |
265 | request.DownloadProgressed -= reportProgress;
266 |
267 | return response;
268 | });
269 |
270 | ///
271 | /// Send a HEAD request to the Tus server.
272 | ///
273 | /// The endpoint to post the HEAD request to.
274 | /// The response from the Tus server.
275 | public async Task HeadAsync(string url)
276 | {
277 | var client = new TusHttpClient();
278 | var request = new TusHttpRequest(url, RequestMethod.Head, AdditionalHeaders);
279 |
280 | try
281 | {
282 | return await client.PerformRequestAsync(request)
283 | .ConfigureAwait(false);
284 | }
285 | catch (TusException ex)
286 | {
287 | return new TusHttpResponse(ex.StatusCode);
288 | }
289 | }
290 |
291 | ///
292 | /// Get information about the Tus server.
293 | ///
294 | /// The URL of the Tus enabled endpoint.
295 | /// A containing information about the Tus server.
296 | /// Throws if request fails.
297 | public async Task GetServerInfo(string url)
298 | {
299 | var client = new TusHttpClient();
300 | var request = new TusHttpRequest(url, RequestMethod.Options, AdditionalHeaders);
301 |
302 | var response = await client.PerformRequestAsync(request)
303 | .ConfigureAwait(false);
304 |
305 | // Spec says NoContent but tusd gives OK because of browser bugs
306 | if (response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.OK)
307 | throw new Exception("getServerInfo failed. " + response.ResponseString);
308 |
309 | response.Headers.TryGetValue(TusHeaderNames.TusResumable, out var version);
310 | response.Headers.TryGetValue(TusHeaderNames.TusVersion, out var supportedVersions);
311 | response.Headers.TryGetValue(TusHeaderNames.TusExtension, out var extensions);
312 | response.Headers.TryGetValue(TusHeaderNames.TusMaxSize, out var maxSizeString);
313 | response.Headers.TryGetValue(TusHeaderNames.TusChecksumAlgorithm, out var checksumAlgorithms);
314 | long.TryParse(maxSizeString, out var maxSize);
315 | return new TusServerInfo(version, supportedVersions, extensions, maxSize, checksumAlgorithms);
316 | }
317 |
318 | ///
319 | /// Delete a file from the Tus server.
320 | ///
321 | /// The URL of the file at the Tus server.
322 | /// A indicating whether the file is deleted.
323 | public async Task Delete(string url)
324 | {
325 | var client = new TusHttpClient();
326 | var request = new TusHttpRequest(url, RequestMethod.Delete, AdditionalHeaders);
327 |
328 | var response = await client.PerformRequestAsync(request)
329 | .ConfigureAwait(false);
330 |
331 | return response.StatusCode == HttpStatusCode.NoContent ||
332 | response.StatusCode == HttpStatusCode.NotFound ||
333 | response.StatusCode == HttpStatusCode.Gone;
334 | }
335 |
336 | private async Task GetFileOffset(string url)
337 | {
338 | var client = new TusHttpClient();
339 | var request = new TusHttpRequest(url, RequestMethod.Head, AdditionalHeaders);
340 |
341 | var response = await client.PerformRequestAsync(request)
342 | .ConfigureAwait(false);
343 |
344 | if (response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.OK)
345 | throw new Exception("GetFileOffset failed. " + response.ResponseString);
346 |
347 | if (!response.Headers.ContainsKey(TusHeaderNames.UploadOffset))
348 | throw new Exception("Offset Header Missing");
349 |
350 | return long.Parse(response.Headers[TusHeaderNames.UploadOffset]);
351 | }
352 |
353 | private static int ChunkSizeToMB(double chunkSize)
354 | {
355 | return (int) Math.Ceiling(chunkSize * 1024.0 * 1024.0);
356 | }
357 | }
358 | }
--------------------------------------------------------------------------------
/src/TusDotNetClient/TusDotNetClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 | TusDotNetClient
5 | TusDotNetClient
6 | TusDotNetClient
7 | jonstodle
8 | A client library for interacting with a Tus enabled server.
9 | https://github.com/jonstodle/TusDotNetClient
10 | https://github.com/jonstodle/TusDotNetClient
11 | MIT
12 | latest
13 | 1.2.0
14 |
15 |
--------------------------------------------------------------------------------
/src/TusDotNetClient/TusException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Net;
5 |
6 | namespace TusDotNetClient
7 | {
8 | ///
9 | /// Represents an exception that might occur when using .
10 | ///
11 | public class TusException : WebException
12 | {
13 | ///
14 | /// Get the content, if any, of the failed operation.
15 | ///
16 | public string ResponseContent { get; }
17 |
18 | ///
19 | /// Get the HTTP status code, if any, of the failed operation.
20 | ///
21 | public HttpStatusCode StatusCode { get; }
22 |
23 | ///
24 | /// Get the description of the HTTP status code.
25 | ///
26 | public string StatusDescription { get; }
27 |
28 | ///
29 | /// Get the original that occured.
30 | ///
31 | public WebException OriginalException { get; }
32 |
33 | ///
34 | /// Create a new instance of based on an .
35 | ///
36 | /// An to base the on.
37 | public TusException(OperationCanceledException ex)
38 | : base(ex.Message, ex, WebExceptionStatus.RequestCanceled, null)
39 | {
40 | OriginalException = null;
41 | }
42 |
43 | ///
44 | /// Create a new instance of based on another .
45 | ///
46 | /// The to base the new on.
47 | /// Text to prefix the with.
48 | public TusException(TusException ex, string message)
49 | : base($"{message}{ex.Message}", ex, ex.Status, ex.Response)
50 | {
51 | OriginalException = ex;
52 |
53 | StatusCode = ex.StatusCode;
54 | StatusDescription = ex.StatusDescription;
55 | ResponseContent = ex.ResponseContent;
56 | }
57 |
58 | ///
59 | /// Create a new instance of based on a .
60 | ///
61 | /// The to base the new on.
62 | /// Text to prefix the with.
63 | public TusException(WebException ex, string message = "")
64 | : base($"{message}{ex.Message}", ex, ex.Status, ex.Response)
65 | {
66 | OriginalException = ex;
67 |
68 | if (ex.Response is HttpWebResponse webResponse &&
69 | webResponse.GetResponseStream() is Stream responseStream)
70 | {
71 | using (var reader = new StreamReader(responseStream))
72 | {
73 | StatusCode = webResponse.StatusCode;
74 | StatusDescription = webResponse.StatusDescription;
75 | ResponseContent = reader.ReadToEnd();
76 | }
77 | }
78 | }
79 |
80 | ///
81 | /// Get a containing all relevant information about the .
82 | ///
83 | /// A containing information about the exception.
84 | public override string ToString()
85 | {
86 | var bits = new List
87 | {
88 | Message
89 | };
90 |
91 | if (Response is WebResponse response)
92 | {
93 | bits.Add($"URL:{response.ResponseUri}");
94 | }
95 |
96 | if (StatusCode != HttpStatusCode.OK)
97 | {
98 | bits.Add($"{StatusCode}:{StatusDescription}");
99 | }
100 |
101 | if (!string.IsNullOrEmpty(ResponseContent))
102 | {
103 | bits.Add(ResponseContent);
104 | }
105 |
106 | return string.Join(Environment.NewLine, bits);
107 | }
108 | }
109 | }
--------------------------------------------------------------------------------
/src/TusDotNetClient/TusHTTPClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace TusDotNetClient
9 | {
10 | ///
11 | /// A class to execute requests against a Tus enabled server.
12 | ///
13 | public class TusHttpClient
14 | {
15 | ///
16 | /// Get or set the proxy to use for requests.
17 | ///
18 | public IWebProxy Proxy { get; set; }
19 |
20 | ///
21 | /// Perform a request to the Tus server.
22 | ///
23 | /// The to execute.
24 | /// A with the response data.
25 | /// Throws when the request fails.
26 | public async Task PerformRequestAsync(TusHttpRequest request)
27 | {
28 | var segment = request.BodyBytes;
29 |
30 | try
31 | {
32 | var webRequest = WebRequest.CreateHttp(request.Url);
33 | webRequest.AutomaticDecompression = DecompressionMethods.GZip;
34 |
35 | webRequest.Timeout = Timeout.Infinite;
36 | webRequest.ReadWriteTimeout = Timeout.Infinite;
37 | webRequest.Method = request.Method;
38 | webRequest.KeepAlive = false;
39 |
40 | webRequest.Proxy = Proxy;
41 |
42 | try
43 | {
44 | webRequest.ServicePoint.Expect100Continue = false;
45 | }
46 | catch (PlatformNotSupportedException)
47 | {
48 | //expected on .net core 2.0 with systemproxy
49 | //fixed by https://github.com/dotnet/corefx/commit/a9e01da6f1b3a0dfbc36d17823a2264e2ec47050
50 | //should work in .net core 2.2
51 | }
52 |
53 | //SEND
54 | var buffer = new byte[4096];
55 |
56 | var totalBytesWritten = 0L;
57 |
58 | webRequest.AllowWriteStreamBuffering = false;
59 | webRequest.ContentLength = segment.Count;
60 |
61 | foreach (var header in request.Headers)
62 | switch (header.Key)
63 | {
64 | case TusHeaderNames.ContentLength:
65 | webRequest.ContentLength = long.Parse(header.Value);
66 | break;
67 | case TusHeaderNames.ContentType:
68 | webRequest.ContentType = header.Value;
69 | break;
70 | default:
71 | webRequest.Headers.Add(header.Key, header.Value);
72 | break;
73 | }
74 |
75 | if (request.BodyBytes.Count > 0)
76 | {
77 | var inputStream = new MemoryStream(request.BodyBytes.Array, request.BodyBytes.Offset,
78 | request.BodyBytes.Count);
79 |
80 | using (var requestStream = webRequest.GetRequestStream())
81 | {
82 | inputStream.Seek(0, SeekOrigin.Begin);
83 |
84 | var bytesWritten = await inputStream.ReadAsync(buffer, 0, buffer.Length, request.CancelToken)
85 | .ConfigureAwait(false);
86 |
87 | request.OnUploadProgressed(0, segment.Count);
88 |
89 | while (bytesWritten > 0)
90 | {
91 | totalBytesWritten += bytesWritten;
92 |
93 | request.OnUploadProgressed(totalBytesWritten, segment.Count);
94 |
95 | await requestStream.WriteAsync(buffer, 0, bytesWritten, request.CancelToken)
96 | .ConfigureAwait(false);
97 |
98 | bytesWritten = await inputStream.ReadAsync(buffer, 0, buffer.Length, request.CancelToken)
99 | .ConfigureAwait(false);
100 | }
101 | }
102 | }
103 |
104 | var response = (HttpWebResponse) await webRequest.GetResponseAsync()
105 | .ConfigureAwait(false);
106 |
107 | //contentLength=0 for gzipped responses due to .net bug
108 | long contentLength = Math.Max(response.ContentLength, 0);
109 |
110 | buffer = new byte[16 * 1024];
111 |
112 | var outputStream = new MemoryStream();
113 |
114 | using (var responseStream = response.GetResponseStream())
115 | {
116 | if (responseStream != null)
117 | {
118 | var bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length, request.CancelToken)
119 | .ConfigureAwait(false);
120 |
121 | request.OnDownloadProgressed(0, contentLength);
122 |
123 | var totalBytesRead = 0L;
124 | while (bytesRead > 0)
125 | {
126 | totalBytesRead += bytesRead;
127 |
128 | request.OnDownloadProgressed(totalBytesRead, contentLength);
129 |
130 | await outputStream.WriteAsync(buffer, 0, bytesRead, request.CancelToken)
131 | .ConfigureAwait(false);
132 |
133 | bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length, request.CancelToken)
134 | .ConfigureAwait(false);
135 | }
136 | }
137 | }
138 |
139 | return new TusHttpResponse(
140 | response.StatusCode,
141 | response.Headers.AllKeys
142 | .ToDictionary(headerName => headerName, headerName => response.Headers.Get(headerName)),
143 | outputStream.ToArray());
144 | }
145 | catch (OperationCanceledException cancelEx)
146 | {
147 | throw new TusException(cancelEx);
148 | }
149 | catch (WebException ex)
150 | {
151 | throw new TusException(ex);
152 | }
153 | }
154 | }
155 | }
--------------------------------------------------------------------------------
/src/TusDotNetClient/TusHeaderNames.cs:
--------------------------------------------------------------------------------
1 | namespace TusDotNetClient
2 | {
3 | ///
4 | /// A collection of the header names used by the Tus protocol.
5 | ///
6 | public static class TusHeaderNames
7 | {
8 | public const string TusResumable = "Tus-Resumable";
9 | public const string TusVersion = "Tus-Version";
10 | public const string TusExtension = "Tus-Extension";
11 | public const string TusMaxSize = "Tus-Max-Size";
12 | public const string TusChecksumAlgorithm = "Tus-Checksum-Algorithm";
13 | public const string UploadLength = "Upload-Length";
14 | public const string UploadOffset = "Upload-Offset";
15 | public const string UploadMetadata = "Upload-Metadata";
16 | public const string UploadChecksum = "Upload-Checksum";
17 | public const string ContentLength = "Content-Length";
18 | public const string ContentType = "Content-Type";
19 | }
20 | }
--------------------------------------------------------------------------------
/src/TusDotNetClient/TusHttpRequest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 |
5 | namespace TusDotNetClient
6 | {
7 | ///
8 | /// HTTP methods supported by
9 | ///
10 | public enum RequestMethod
11 | {
12 | Get,
13 | Post,
14 | Head,
15 | Patch,
16 | Options,
17 | Delete
18 | }
19 |
20 | ///
21 | /// A class representing a request to be sent to a Tus enabled server.
22 | ///
23 | public class TusHttpRequest
24 | {
25 | private readonly Dictionary _headers;
26 |
27 | ///
28 | /// Occurs when progress sending the request is made.
29 | ///
30 | public event ProgressDelegate UploadProgressed;
31 |
32 | ///
33 | /// Occurs when progress receiving the response is made.
34 | ///
35 | public event ProgressDelegate DownloadProgressed;
36 |
37 | ///
38 | /// Get the URL the request is being made against.
39 | ///
40 | public Uri Url { get; }
41 | ///
42 | /// Get the HTTP method of the request.
43 | ///
44 | public string Method { get; }
45 | ///
46 | /// Get a read-only collection of the headers of the request.
47 | ///
48 | public IReadOnlyDictionary Headers => _headers;
49 | ///
50 | /// Get the raw bytes of the content of the request.
51 | ///
52 | public ArraySegment BodyBytes { get; }
53 | ///
54 | /// Get the cancellation token used to cancel the request.
55 | ///
56 | public CancellationToken CancelToken { get; }
57 |
58 | ///
59 | /// Create a new request to be made against a Tus enabled server.
60 | ///
61 | /// The URL to make the request against.
62 | /// The HTTP method to use for the request.
63 | /// A of user specified headers to add to the request.
64 | /// A byte array of the content of the request.
65 | /// A to cancel the request with.
66 | public TusHttpRequest(
67 | string url,
68 | RequestMethod method,
69 | IDictionary additionalHeaders = null,
70 | ArraySegment bodyBytes = default,
71 | CancellationToken? cancelToken = null)
72 | {
73 | Url = new Uri(url);
74 | Method = method.ToString().ToUpperInvariant();
75 | BodyBytes = bodyBytes;
76 | CancelToken = cancelToken ?? CancellationToken.None;
77 |
78 | _headers = additionalHeaders is null
79 | ? new Dictionary(1)
80 | : new Dictionary(additionalHeaders);
81 | _headers.Add(TusHeaderNames.TusResumable, "1.0.0");
82 | }
83 |
84 | ///
85 | /// Add an HTTP header to request.
86 | ///
87 | /// The name of the HTTP header.
88 | /// The value of the HTTP header.
89 | public void AddHeader(string key, string value) => _headers.Add(key, value);
90 |
91 | ///
92 | /// Invoke an event.
93 | ///
94 | /// The number of bytes uploaded so far.
95 | /// The total number of bytes to be uploaded.
96 | public void OnUploadProgressed(long bytesTransferred, long bytesTotal) =>
97 | UploadProgressed?.Invoke(bytesTransferred, bytesTotal);
98 |
99 | ///
100 | /// Invoke an event.
101 | ///
102 | /// The number of bytes downloaded so far.
103 | /// The total number of bytes to be downloaded.
104 | public void OnDownloadProgressed(long bytesTransferred, long bytesTotal) =>
105 | DownloadProgressed?.Invoke(bytesTransferred, bytesTotal);
106 | }
107 | }
--------------------------------------------------------------------------------
/src/TusDotNetClient/TusHttpResponse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net;
4 | using System.Text;
5 |
6 | namespace TusDotNetClient
7 | {
8 | ///
9 | /// Represents a response from a request made to a Tus enabled server.
10 | ///
11 | public class TusHttpResponse
12 | {
13 | ///
14 | /// Get the HTTP status code from the Tus server.
15 | ///
16 | public HttpStatusCode StatusCode { get; }
17 | ///
18 | /// Get the HTTP headers from the response.
19 | ///
20 | public IReadOnlyDictionary Headers { get; }
21 | ///
22 | /// Get the content of the HTTP response as bytes.
23 | ///
24 | public byte[] ResponseBytes { get; }
25 | ///
26 | /// Get the content of the HTTP response as a .
27 | ///
28 | public string ResponseString => Encoding.UTF8.GetString(ResponseBytes);
29 |
30 | ///
31 | /// Create an instance of a .
32 | ///
33 | /// The HTTP status code of the response.
34 | /// The HTTP headers of the response.
35 | /// The content of the response.
36 | public TusHttpResponse(
37 | HttpStatusCode statusCode,
38 | IDictionary headers = null,
39 | byte[] responseBytes = null)
40 | {
41 | StatusCode = statusCode;
42 | Headers = headers is null
43 | ? new Dictionary(0)
44 | : new Dictionary(headers);
45 | ResponseBytes = responseBytes;
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/TusDotNetClient/TusOperation.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | using System.Threading.Tasks;
3 |
4 | namespace TusDotNetClient
5 | {
6 | ///
7 | /// A delegate used for reporting progress of a transfer of bytes.
8 | ///
9 | /// The number of bytes transferred so far.
10 | /// The total number of bytes to transfer.
11 | public delegate void ProgressDelegate(long bytesTransferred, long bytesTotal);
12 |
13 | ///
14 | /// Represents an operation against a Tus enabled server. supports progress reports.
15 | ///
16 | /// The type of the operation result.
17 | public class TusOperation
18 | {
19 | private readonly OperationDelegate _operation;
20 | private Task _operationTask;
21 |
22 | ///
23 | /// Represents an operation which receives a delegate to report transfer progress to.
24 | ///
25 | /// A delegate which transfer progress can be reported to.
26 | public delegate Task OperationDelegate(ProgressDelegate reportProgress);
27 |
28 | ///
29 | /// Occurs when progress sending the request is made.
30 | ///
31 | public event ProgressDelegate Progressed;
32 |
33 | ///
34 | /// Get the asynchronous operation to be performed. This will initiate the operation.
35 | ///
36 | public Task Operation =>
37 | _operationTask ??
38 | (_operationTask = _operation((transferred, total) =>
39 | Progressed?.Invoke(transferred, total)));
40 |
41 | ///
42 | /// Create an instance of a
43 | ///
44 | /// The operation to perform.
45 | internal TusOperation(OperationDelegate operation)
46 | {
47 | _operation = operation;
48 | }
49 |
50 | ///
51 | /// Gets an awaiter used to initiate and await the operation.
52 | ///
53 | /// The of the underlying .
54 | public TaskAwaiter GetAwaiter() => Operation.GetAwaiter();
55 | }
56 | }
--------------------------------------------------------------------------------
/src/TusDotNetClient/TusServerInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 |
3 | namespace TusDotNetClient
4 | {
5 | ///
6 | /// Represents information about a Tus enabled server.
7 | ///
8 | public class TusServerInfo
9 | {
10 | ///
11 | /// Get the version of the Tus protocol used by the Tus server.
12 | ///
13 | public string Version { get; }
14 | ///
15 | /// Get a list of versions of the protocol the Tus server supports.
16 | ///
17 | public string[] SupportedVersions { get; }
18 | ///
19 | /// Get the protocol extensions supported by the Tus server.
20 | ///
21 | public string[] Extensions { get; }
22 | ///
23 | /// Get the maximum total size of a single file supported by the Tus server.
24 | ///
25 | public long MaxSize { get; }
26 | ///
27 | /// Get the checksum algorithms supported by the Tus server.
28 | ///
29 | public string[] SupportedChecksumAlgorithms { get; }
30 |
31 | ///
32 | /// Get whether the termination protocol extension is supported by the Tus server.
33 | ///
34 | public bool SupportsDelete => Extensions.Contains("termination");
35 |
36 | ///
37 | /// Create a new instance of .
38 | ///
39 | /// The protocol version used by the Tus server.
40 | /// The versions of the protocol supported by the Tus server separated by commas.
41 | /// The protocol extensions supported by the Tus server separated by commas.
42 | /// The maximum total size of a single file supported by the Tus server.
43 | /// The checksum algorithms supported by the Tus server separated by the Tus server.
44 | public TusServerInfo(
45 | string version,
46 | string supportedVersions,
47 | string extensions,
48 | long? maxSize,
49 | string checksumAlgorithms)
50 | {
51 | Version = version ?? "";
52 | SupportedVersions = (supportedVersions ?? "").Trim().Split(',').ToArray();
53 | Extensions = (extensions ?? "").Trim().Split(',').ToArray();
54 | MaxSize = maxSize ?? 0;
55 | SupportedChecksumAlgorithms = (checksumAlgorithms ?? "").Trim().Split(',').ToArray();
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/tests/TusDotNetClientTests/Fixture.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Linq;
6 |
7 | namespace TusDotNetClientTests
8 | {
9 | public class Fixture : IDisposable
10 | {
11 | private readonly Process _tusProcess;
12 |
13 | public static readonly DirectoryInfo DataDirectory;
14 |
15 | static Fixture()
16 | {
17 | DataDirectory = Directory.CreateDirectory("data");
18 |
19 | var smallTextFile = new FileInfo(Path.Combine(DataDirectory.FullName, "small_text_file.txt"));
20 | File.WriteAllText(smallTextFile.FullName, Guid.NewGuid().ToString());
21 |
22 | var largeSampleFile = new FileInfo(Path.Combine(DataDirectory.FullName, "large_sample_file.bin"));
23 | using (var fileStream = new FileStream(largeSampleFile.FullName, FileMode.Create, FileAccess.Write))
24 | {
25 | var bytes = new byte[1024 * 1024];
26 | foreach (var _ in Enumerable.Range(0, 50))
27 | {
28 | new Random().NextBytes(bytes);
29 | fileStream.Write(bytes, 0, bytes.Length);
30 | }
31 | }
32 |
33 | TestFiles = new[]
34 | {
35 | new [] {smallTextFile},
36 | new [] {largeSampleFile},
37 | };
38 | }
39 |
40 | public Fixture()
41 | {
42 | _tusProcess = Process.Start(new DirectoryInfo(Directory.GetCurrentDirectory())
43 | .Parent?
44 | .Parent?
45 | .Parent?
46 | .EnumerateFiles("tusd*")
47 | .FirstOrDefault()?
48 | .FullName ??
49 | throw new ArgumentException(
50 | "tusd executable must be present in test project directory"));
51 | }
52 |
53 | public static IEnumerable