├── .config └── dotnet-tools.json ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature_request.yaml └── pull_request_template.md ├── .gitignore ├── .paket ├── Paket.Restore.targets ├── paket.bootstrapper.exe └── paket.targets ├── .travis.yml ├── LICENSE.txt ├── Owin.Compression.sln ├── README.md ├── RELEASE_NOTES.md ├── appveyor.yml ├── build.cmd ├── build.fsx ├── build.sh ├── docs ├── content │ ├── content │ │ └── fsdocs-custom.css │ ├── index.fsx │ └── tutorial.fsx └── files │ └── img │ ├── logo-template.pdn │ └── logo.png ├── lib └── README.md ├── netfx.props ├── nuget.Config ├── paket.dependencies ├── paket.lock ├── screen.png ├── src ├── Owin.Compression.Standard │ ├── AssemblyInfo.fs │ ├── CompressionModule.fs │ └── Owin.Compression.Standard.fsproj └── Owin.Compression │ ├── AssemblyInfo.fs │ ├── CompressionModule.fs │ ├── Owin.Compression.fsproj │ ├── Script.fsx │ ├── nuget.Config │ ├── paket.references │ └── paket.template └── tests ├── Aspnet.Core.WebAPI.Test ├── Aspnet.Core.WebAPI.Test.fsproj ├── Aspnet.Core.WebAPI.Test.sln ├── Controllers │ └── WeatherForecastController.fs ├── Program.fs ├── Properties │ └── launchSettings.json ├── WeatherForecast.fs ├── appsettings.Development.json ├── appsettings.json └── paket.references └── Owin.Compression.Tests ├── App.config ├── Owin.Compression.Tests.fsproj ├── Tests.fs └── paket.references /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "sourcelink": { 6 | "version": "3.1.1", 7 | "commands": [ 8 | "sourcelink" 9 | ] 10 | }, 11 | "dotnet-reportgenerator-globaltool": { 12 | "version": "4.2.15", 13 | "commands": [ 14 | "reportgenerator" 15 | ] 16 | }, 17 | "paket": { 18 | "version": "8.0.0", 19 | "commands": [ 20 | "paket" 21 | ] 22 | }, 23 | "fake-cli": { 24 | "version": "5.23.1", 25 | "commands": [ 26 | "fake" 27 | ] 28 | }, 29 | "fcswatch-cli": { 30 | "version": "0.7.14", 31 | "commands": [ 32 | "fcswatch" 33 | ] 34 | }, 35 | "fsharp-analyzers": { 36 | "version": "0.11.0", 37 | "commands": [ 38 | "fsharp-analyzers" 39 | ] 40 | }, 41 | "dotnet-serve": { 42 | "version": "1.10.128", 43 | "commands": [ 44 | "dotnet-serve" 45 | ] 46 | }, 47 | "fsdocs-tool": { 48 | "version": "16.1.1", 49 | "commands": [ 50 | "fsdocs" 51 | ] 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.fs] 16 | max_line_length=300 17 | fsharp_space_before_parameter=false 18 | fsharp_space_before_lowercase_invocation=false 19 | fsharp_space_after_comma=false 20 | fsharp_space_after_semicolon=false 21 | fsharp_space_around_delimiter=false 22 | fsharp_max_if_then_else_short_width=300 23 | fsharp_max_infix_operator_expression=300 24 | fsharp_max_record_width=300 25 | fsharp_max_record_number_of_items=300 26 | fsharp_max_array_or_list_width=300 27 | fsharp_max_array_or_list_number_of_items=300 28 | fsharp_max_value_binding_width=300 29 | fsharp_max_function_binding_width=300 30 | fsharp_max_dot_get_expression_width=300 31 | fsharp_multiline_block_brackets_on_same_column=true 32 | fsharp_blank_lines_around_nested_multiline_expressions=false 33 | fsharp_experimental_stroustrup_style=true 34 | fsharp_keep_max_number_of_blank_lines=3 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp text=auto eol=lf 6 | *.vb diff=csharp text=auto eol=lf 7 | *.fs diff=csharp text=auto eol=lf 8 | *.fsi diff=csharp text=auto eol=lf 9 | *.fsx diff=csharp text=auto eol=lf 10 | *.sln text eol=crlf merge=union 11 | *.csproj merge=union 12 | *.vbproj merge=union 13 | *.fsproj merge=union 14 | *.dbproj merge=union 15 | 16 | # Standard to msysgit 17 | *.doc diff=astextplain 18 | *.DOC diff=astextplain 19 | *.docx diff=astextplain 20 | *.DOCX diff=astextplain 21 | *.dot diff=astextplain 22 | *.DOT diff=astextplain 23 | *.pdf diff=astextplain 24 | *.PDF diff=astextplain 25 | *.rtf diff=astextplain 26 | *.RTF diff=astextplain 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | title: "[Bug]: " 4 | body: 5 | - type: textarea 6 | id: what-happened 7 | attributes: 8 | label: Describe the bug 9 | description: A clear and concise description of what the bug is. If applicable, add screenshots to help explain your problem. 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: repro-steps 14 | attributes: 15 | label: To Reproduce 16 | description: Steps to reproduce the behavior 17 | value: | 18 | 1. Go to '...' 19 | 2. Click on '...' 20 | 3. Scroll down to '...' 21 | 4. See error 22 | render: shell 23 | - type: textarea 24 | id: expected-behavior 25 | attributes: 26 | label: Expected behavior 27 | description: A clear and concise description of what you expected to happen. 28 | render: shell 29 | - type: textarea 30 | id: desktop-info 31 | attributes: 32 | label: Desktop 33 | value: | 34 | - OS: [e.g. Windows 11] 35 | - Browser: [e.g. Chrome, Edge] 36 | - Version: [e.g. 122] 37 | render: shell 38 | - type: textarea 39 | id: smartphone-info 40 | attributes: 41 | label: Smartphone 42 | value: | 43 | - Device: [e.g. iPhone 15] 44 | - OS: [e.g. iOS 17.4] 45 | - Browser: [e.g. Stock browser, Safari] 46 | - Version: [e.g. 122] 47 | render: shell 48 | - type: textarea 49 | id: additional-ctx 50 | attributes: 51 | label: Additional context 52 | description: Add any other context about the problem here. 53 | render: shell 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: "[Feature]: " 4 | body: 5 | - type: textarea 6 | id: feature-info 7 | attributes: 8 | label: Is your feature request related to a problem? Please describe. 9 | description: A clear and concise description of what the problem is. Add any screenshots about the feature request here. 10 | value: "I'm always frustrated when [...]" 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: expected-solution 15 | attributes: 16 | label: Describe the solution you'd like 17 | description: A clear and concise description of what you want to happen. 18 | render: shell 19 | - type: textarea 20 | id: alternatives-info 21 | attributes: 22 | label: Describe alternatives you've considered 23 | description: A clear and concise description of any alternative solutions or features you've considered. 24 | render: shell 25 | - type: textarea 26 | id: additional-ctx 27 | attributes: 28 | label: Additional context 29 | description: Add any other context about the feature request here. 30 | render: shell 31 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] Build and tests pass locally 20 | - [ ] I have added tests that prove my fix is effective or that my feature works (if appropriate) 21 | - [ ] I have added necessary documentation (if appropriate) 22 | 23 | ## Further comments 24 | 25 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 26 | -------------------------------------------------------------------------------- /.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 | 9 | # Xamarin Studio / monodevelop user-specific 10 | *.userprefs 11 | *.dll.mdb 12 | *.exe.mdb 13 | 14 | # Build results 15 | 16 | [Dd]ebug/ 17 | [Rr]elease/ 18 | x64/ 19 | build/ 20 | [Bb]in/ 21 | [Oo]bj/ 22 | 23 | # MSTest test Results 24 | [Tt]est[Rr]esult*/ 25 | [Bb]uild[Ll]og.* 26 | BenchmarkDotNet.Artifacts/ 27 | 28 | *_i.c 29 | *_p.c 30 | *.ilk 31 | *.meta 32 | *.obj 33 | *.pch 34 | *.pdb 35 | *.pgc 36 | *.pgd 37 | *.rsp 38 | *.sbr 39 | *.tlb 40 | *.tli 41 | *.tlh 42 | *.tmp 43 | *.tmp_proj 44 | *.log 45 | *.vspscc 46 | *.vssscc 47 | .builds 48 | *.pidb 49 | *.log 50 | *.scc 51 | *.bak 52 | 53 | # Visual C++ cache files 54 | ipch/ 55 | *.aps 56 | *.ncb 57 | *.opensdf 58 | *.sdf 59 | *.cachefile 60 | 61 | # Visual Studio profiler 62 | *.psess 63 | *.vsp 64 | *.vspx 65 | 66 | # Other Visual Studio data 67 | .vs/ 68 | 69 | # Guidance Automation Toolkit 70 | *.gpState 71 | 72 | # ReSharper is a .NET coding add-in 73 | _ReSharper*/ 74 | *.[Rr]e[Ss]harper 75 | 76 | # TeamCity is a build add-in 77 | _TeamCity* 78 | 79 | # DotCover is a Code Coverage Tool 80 | *.dotCover 81 | 82 | # NCrunch 83 | *.ncrunch* 84 | .*crunch*.local.xml 85 | 86 | # Installshield output folder 87 | [Ee]xpress/ 88 | 89 | # DocProject is a documentation generator add-in 90 | DocProject/buildhelp/ 91 | DocProject/Help/*.HxT 92 | DocProject/Help/*.HxC 93 | DocProject/Help/*.hhc 94 | DocProject/Help/*.hhk 95 | DocProject/Help/*.hhp 96 | DocProject/Help/Html2 97 | DocProject/Help/html 98 | 99 | # Click-Once directory 100 | publish/ 101 | 102 | # Publish Web Output 103 | *.Publish.xml 104 | 105 | # Enable nuget.exe in the .nuget folder (though normally executables are not tracked) 106 | !.nuget/NuGet.exe 107 | 108 | # Windows Azure Build Output 109 | csx 110 | *.build.csdef 111 | 112 | # Windows Store app package directory 113 | AppPackages/ 114 | 115 | # Others 116 | sql/ 117 | *.Cache 118 | ClientBin/ 119 | [Ss]tyle[Cc]op.* 120 | ~$* 121 | *~ 122 | *.dbmdl 123 | *.[Pp]ublish.xml 124 | *.pfx 125 | *.publishsettings 126 | 127 | # RIA/Silverlight projects 128 | Generated_Code/ 129 | 130 | # Backup & report files from converting an old project file to a newer 131 | # Visual Studio version. Backup files are not needed, because we have git ;-) 132 | _UpgradeReport_Files/ 133 | Backup*/ 134 | UpgradeLog*.XML 135 | UpgradeLog*.htm 136 | 137 | # SQL Server files 138 | App_Data/*.mdf 139 | App_Data/*.ldf 140 | 141 | 142 | #LightSwitch generated files 143 | GeneratedArtifacts/ 144 | _Pvt_Extensions/ 145 | ModelManifest.xml 146 | 147 | # ========================= 148 | # Windows detritus 149 | # ========================= 150 | 151 | # Windows image file caches 152 | Thumbs.db 153 | ehthumbs.db 154 | 155 | # Folder config file 156 | Desktop.ini 157 | 158 | # Recycle Bin used on file shares 159 | $RECYCLE.BIN/ 160 | 161 | # Mac desktop service store files 162 | .DS_Store 163 | 164 | # =================================================== 165 | # Exclude F# project specific directories and files 166 | # =================================================== 167 | 168 | # NuGet Packages Directory 169 | packages/ 170 | 171 | # Generated documentation folder 172 | docs/output/ 173 | 174 | # Temp folder used for publishing docs 175 | temp/ 176 | 177 | # Test results produced by build 178 | TestResults.xml 179 | 180 | # Nuget outputs 181 | nuget/*.nupkg 182 | release.cmd 183 | release.sh 184 | localpackages/ 185 | paket-files 186 | *.orig 187 | .paket/paket.exe 188 | docs/content/license.md 189 | docs/content/release-notes.md 190 | .fake 191 | docs/tools/FSharp.Formatting.svclog 192 | .fsdocs/ -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | $(MSBuildVersion) 10 | 15.0.0 11 | false 12 | true 13 | 14 | true 15 | $(MSBuildThisFileDirectory) 16 | $(MSBuildThisFileDirectory)..\ 17 | $(PaketRootPath)paket-files\paket.restore.cached 18 | $(PaketRootPath)paket.lock 19 | classic 20 | proj 21 | assembly 22 | native 23 | /Library/Frameworks/Mono.framework/Commands/mono 24 | mono 25 | 26 | 27 | $(PaketRootPath)paket.bootstrapper.exe 28 | $(PaketToolsPath)paket.bootstrapper.exe 29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 30 | 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | True 42 | 43 | 44 | False 45 | 46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $(PaketRootPath)paket 56 | $(PaketToolsPath)paket 57 | 58 | 59 | 60 | 61 | 62 | $(PaketRootPath)paket.exe 63 | $(PaketToolsPath)paket.exe 64 | 65 | 66 | 67 | 68 | 69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) 70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) 71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <_PaketCommand>dotnet paket 83 | 84 | 85 | 86 | 87 | 88 | $(PaketToolsPath)paket 89 | $(PaketBootStrapperExeDir)paket 90 | 91 | 92 | paket 93 | 94 | 95 | 96 | 97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" 99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 123 | false 124 | true 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 134 | 135 | 136 | 137 | 138 | 139 | 141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) 142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) 143 | 144 | 145 | 146 | 147 | %(PaketRestoreCachedKeyValue.Value) 148 | %(PaketRestoreCachedKeyValue.Value) 149 | 150 | 151 | 152 | 153 | true 154 | false 155 | true 156 | 157 | 158 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached 183 | 184 | $(MSBuildProjectFullPath).paket.references 185 | 186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 187 | 188 | $(MSBuildProjectDirectory)\paket.references 189 | 190 | false 191 | true 192 | true 193 | references-file-or-cache-not-found 194 | 195 | 196 | 197 | 198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 200 | references-file 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | true 212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | false 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 240 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6]) 241 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7]) 242 | 243 | 244 | %(PaketReferencesFileLinesInfo.PackageVersion) 245 | All 246 | runtime 247 | $(ExcludeAssets);contentFiles 248 | $(ExcludeAssets);build;buildMultitargeting;buildTransitive 249 | true 250 | true 251 | 252 | 253 | 254 | 255 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 265 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 266 | 267 | 268 | %(PaketCliToolFileLinesInfo.PackageVersion) 269 | 270 | 271 | 272 | 276 | 277 | 278 | 279 | 280 | 281 | false 282 | 283 | 284 | 285 | 286 | 287 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> 288 | 289 | 290 | 291 | 292 | 293 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 294 | true 295 | false 296 | true 297 | false 298 | true 299 | false 300 | true 301 | false 302 | true 303 | false 304 | true 305 | $(PaketIntermediateOutputPath)\$(Configuration) 306 | $(PaketIntermediateOutputPath) 307 | 308 | 309 | 310 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 370 | 371 | 420 | 421 | 466 | 467 | 511 | 512 | 555 | 556 | 557 | 558 | -------------------------------------------------------------------------------- /.paket/paket.bootstrapper.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thorium/Owin.Compression/67fac5ce33b33f2538457538e69042dbd94233ed/.paket/paket.bootstrapper.exe -------------------------------------------------------------------------------- /.paket/paket.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | true 8 | $(MSBuildThisFileDirectory) 9 | $(MSBuildThisFileDirectory)..\ 10 | 11 | 12 | 13 | $(PaketToolsPath)paket.exe 14 | $(PaketToolsPath)paket.bootstrapper.exe 15 | "$(PaketExePath)" 16 | mono --runtime=v4.0.30319 "$(PaketExePath)" 17 | "$(PaketBootStrapperExePath)" 18 | mono --runtime=v4.0.30319 $(PaketBootStrapperExePath) 19 | 20 | $(PaketCommand) restore 21 | $(PaketBootStrapperCommand) 22 | 23 | RestorePackages; $(BuildDependsOn); 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | 3 | sudo: false # use the new container-based Travis infrastructure 4 | 5 | before_install: 6 | - chmod +x build.sh 7 | 8 | script: 9 | - ./build.sh All 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Owin.Compression.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.7.34031.279 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{63297B98-5CED-492C-A5B7-A5B4F73CF142}" 6 | ProjectSection(SolutionItems) = preProject 7 | paket.dependencies = paket.dependencies 8 | paket.lock = paket.lock 9 | EndProjectSection 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{A6A6AF7D-D6E3-442D-9B1E-58CC91879BE1}" 12 | EndProject 13 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Owin.Compression", "src\Owin.Compression\Owin.Compression.fsproj", "{1869DF4F-917C-4051-9D88-3701561B13FC}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "project", "project", "{BF60BC93-E09B-4E5F-9D85-95A519479D54}" 16 | ProjectSection(SolutionItems) = preProject 17 | build.fsx = build.fsx 18 | README.md = README.md 19 | RELEASE_NOTES.md = RELEASE_NOTES.md 20 | EndProjectSection 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{83F16175-43B1-4C90-A1EE-8E351C33435D}" 23 | ProjectSection(SolutionItems) = preProject 24 | docs\tools\generate.fsx = docs\tools\generate.fsx 25 | docs\tools\templates\template.cshtml = docs\tools\templates\template.cshtml 26 | EndProjectSection 27 | EndProject 28 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "content", "content", "{8E6D5255-776D-4B61-85F9-73C37AA1FB9A}" 29 | ProjectSection(SolutionItems) = preProject 30 | docs\content\index.fsx = docs\content\index.fsx 31 | docs\content\tutorial.fsx = docs\content\tutorial.fsx 32 | EndProjectSection 33 | EndProject 34 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ED8079DD-2B06-4030-9F0F-DC548F98E1C4}" 35 | EndProject 36 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Owin.Compression.Tests", "tests\Owin.Compression.Tests\Owin.Compression.Tests.fsproj", "{FFF97E19-5445-4F64-8F28-6A1B86D4E158}" 37 | EndProject 38 | Global 39 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 40 | Debug|Any CPU = Debug|Any CPU 41 | Release|Any CPU = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 44 | {1869DF4F-917C-4051-9D88-3701561B13FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {1869DF4F-917C-4051-9D88-3701561B13FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {1869DF4F-917C-4051-9D88-3701561B13FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {1869DF4F-917C-4051-9D88-3701561B13FC}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {FFF97E19-5445-4F64-8F28-6A1B86D4E158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {FFF97E19-5445-4F64-8F28-6A1B86D4E158}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {FFF97E19-5445-4F64-8F28-6A1B86D4E158}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {FFF97E19-5445-4F64-8F28-6A1B86D4E158}.Release|Any CPU.Build.0 = Release|Any CPU 52 | EndGlobalSection 53 | GlobalSection(SolutionProperties) = preSolution 54 | HideSolutionNode = FALSE 55 | EndGlobalSection 56 | GlobalSection(NestedProjects) = preSolution 57 | {83F16175-43B1-4C90-A1EE-8E351C33435D} = {A6A6AF7D-D6E3-442D-9B1E-58CC91879BE1} 58 | {8E6D5255-776D-4B61-85F9-73C37AA1FB9A} = {A6A6AF7D-D6E3-442D-9B1E-58CC91879BE1} 59 | {FFF97E19-5445-4F64-8F28-6A1B86D4E158} = {ED8079DD-2B06-4030-9F0F-DC548F98E1C4} 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Owin.Compression 3 | 4 | Compression (Deflate / GZip) module for Microsoft OWIN Selfhost filesystem pipeline. 5 | It can also be used with AspNetCore, e.g. with .NET8.0 and Kestrel. 6 | 7 | With this module, you can compress (deflate or gzip) large files (like concatenated *.js or *.css files) to reduce the amount of web traffic. 8 | It supports eTag caching: If the client's sent hashcode is a match, send 302 instead of re-sending the same content. 9 | 10 | It also supports streaming responses. The config allows you to disable deflate and streaming if you prefer. 11 | 12 | This project works on C# and F# and should work on all .NET platforms, also on Windows, and even Mono as well. 13 | 14 | 15 | Here is a demo in action from Fiddler net traffic monitor: 16 | 17 |  18 | 19 | Read the [Getting started tutorial](https://thorium.github.io/Owin.Compression/index.html#Getting-started) to learn more. 20 | 21 | Documentation: https://thorium.github.io/Owin.Compression 22 | 23 | 24 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 1.0.47 - December 13 2024 2 | * Nuget package infromation update 3 | 4 | ### 1.0.46 - June 21 2024 5 | * Performance optimizations when on .NET Standard 2.1 (NET 6.0 / NET 8.0) 6 | 7 | ### 1.0.45 - April 1 2024 8 | * NuGet Net Framework 4.7.2 dependency corrected to version 4.8 9 | 10 | ### 1.0.44 - April 1 2024 11 | * More tolerant pipeline config. 12 | * Minor performance improvements. 13 | * Package frameworks to .NET 4.8, .NET Standard 2.0, and 2.1 14 | 15 | ### 1.0.41 - March 22 2024 16 | * ASP.NET Core side reference updates 17 | 18 | ### 1.0.40 - March 22 2024 19 | * Reference component updates, PR #13 20 | * Package frameworks to .NET 4.7.2, .NET Standard 2.0, and 2.1 21 | 22 | ### 1.0.38 - November 1 2023 23 | * Less exception catching on runtime 24 | 25 | ### 1.0.37 - November 1 2023 26 | * Minor performance improvements 27 | * FSharp.Core update 28 | 29 | ### 1.0.34 - October 7 2023 30 | * Minor performance improvements 31 | 32 | ### 1.0.33 - March 7 2023 33 | * Dependency update: Microsoft.Extensions.Primitives 34 | 35 | ### 1.0.32 - November 23 2022 36 | * Improvements on error handling 37 | * Improvements on picking which files to compress 38 | 39 | ### 1.0.30 - November 18 2022 40 | * StreamingDisabled setting for disabling HTTP1.1 streaming responses 41 | * Streaming added on static files 42 | * AspNET Core WebAPI .NET Standard fixes (and test project added) 43 | 44 | ### 1.0.29 - August 11 2022 45 | * Package dependency update 46 | 47 | ### 1.0.28 - August 10 2022 48 | * VS2022 update 49 | * Fixed task compilation 50 | 51 | ### 1.0.26 - June 17 2022 52 | * Reference component update 53 | * Better performance via F# 6.0 tasks 54 | 55 | ### 1.0.24 - March 22 2022 56 | * Fix for avoiding double compression 57 | 58 | ### 1.0.23 - July 13 2021 59 | * Reference component update 60 | 61 | ### 1.0.22 - February 01 2021 62 | * Reference component update 63 | 64 | ### 1.0.21 - July 11 2018 65 | * Reference component update 66 | 67 | ### 1.0.20 - February 20 2018 68 | * Check for cancellation token before reading headers 69 | 70 | ### 1.0.19 - February 20 2018 71 | * References updated 72 | 73 | ### 1.0.18 - November 08 2017 74 | * Fix for stream ETAGs #8 75 | 76 | ### 1.0.17 - October 02 2017 77 | * Fix for Owin dependency #7 78 | * Initial conversion for .NET Standard 2.0 using Microsoft.AspNetCore.Http, not tested yet. 79 | 80 | ### 1.0.16 - June 09 2017 81 | * References updated 82 | 83 | ### 1.0.15 - April 26 2017 84 | * References updated 85 | 86 | ### 1.0.14 - March 20 2017 87 | * Minor default config update 88 | 89 | ### 1.0.13 - March 08 2017 90 | * No functionality changes. 91 | * Dependency updated. 92 | 93 | ### 1.0.12 - September 10 2016 94 | * Respect Pragma no-cache 95 | 96 | ### 1.0.11 - September 06 2016 97 | * Added compression based on Mime type 98 | * eTag cache: cancel work if can send 304 99 | 100 | ### 1.0.10 - June 30 2016 101 | * Added setting option DeflateDisabled 102 | 103 | ### 1.0.9 - June 29 2016 104 | * Added reference to FSharp.Core 105 | 106 | ### 1.0.8 - April 15 2016 107 | * Added Vary-header part 3. 108 | 109 | ### 1.0.7 - April 15 2016 110 | * Added Vary-header part 2. 111 | 112 | ### 1.0.6 - April 15 2016 113 | * Added Vary-header. 114 | 115 | ### 1.0.5 - April 06 2016 116 | * Don't compress SignalR requests 117 | 118 | ### 1.0.4 - April 06 2016 119 | * Better handling of canceled request. 120 | 121 | ### 1.0.3 - April 04 2016 122 | * Better handling of canceled request. 123 | 124 | ### 1.0.2 - March 11 2016 125 | * Added support for static files with app.UseCompressionModule() 126 | 127 | ### 1.0.1 - December 11 2015 128 | * Documentation and C# interface improved 129 | 130 | ### 1.0 - December 11 2015 131 | * Initial release 132 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | build_script: 4 | - cmd: build.cmd 5 | test: off 6 | version: 0.0.1.{build} 7 | artifacts: 8 | - path: bin 9 | name: bin 10 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | .paket\paket.bootstrapper.exe 5 | if errorlevel 1 ( 6 | exit /b %errorlevel% 7 | ) 8 | 9 | .paket\paket.exe restore 10 | if errorlevel 1 ( 11 | exit /b %errorlevel% 12 | ) 13 | 14 | IF NOT EXIST build.fsx ( 15 | .paket\paket.exe update 16 | packages\build\FAKE\tools\FAKE.exe init.fsx 17 | ) 18 | packages\build\FAKE\tools\FAKE.exe build.fsx %* 19 | -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------- 2 | // FAKE build script 3 | // -------------------------------------------------------------------------------------- 4 | 5 | #r @"packages/build/FAKE/tools/FakeLib.dll" 6 | open Fake 7 | open Fake.Git 8 | open Fake.AssemblyInfoFile 9 | open Fake.ReleaseNotesHelper 10 | open Fake.UserInputHelper 11 | open System 12 | open System.IO 13 | #if MONO 14 | #else 15 | #load "packages/build/SourceLink.Fake/tools/Fake.fsx" 16 | open SourceLink 17 | #endif 18 | 19 | // -------------------------------------------------------------------------------------- 20 | // START TODO: Provide project-specific details below 21 | // -------------------------------------------------------------------------------------- 22 | 23 | // Information about the project are used 24 | // - for version and project name in generated AssemblyInfo file 25 | // - by the generated NuGet package 26 | // - to run tests and to publish documentation on GitHub gh-pages 27 | // - for documentation, you also need to edit info in "docs/tools/generate.fsx" 28 | 29 | // The name of the project 30 | // (used by attributes in AssemblyInfo, name of a NuGet package and directory in 'src') 31 | let project = "Owin.Compression" 32 | 33 | // Short summary of the project 34 | // (used as description in AssemblyInfo and as a short summary for NuGet package) 35 | let summary = "Compression (Deflate / GZip) module for Microsoft OWIN Selfhost filesystem pipeline." 36 | 37 | // Longer description of the project 38 | // (used as a description for NuGet package; line breaks are automatically cleaned up) 39 | let description = "Compression (Deflate / GZip) module for Microsoft OWIN self host web server. With this module you can compress, deflate / gzip large files (like concatenated *.js or *.css files) to reduce amount of web traffic." 40 | 41 | // List of author names (for NuGet package) 42 | let authors = [ "Tuomas Hietanen" ] 43 | 44 | // Tags for your project (for NuGet package) 45 | let tags = "OWIN SelfHost GZip Deflate compress pack self host file system pipeline Microsoft" 46 | 47 | // File system information 48 | let solutionFile = "Owin.Compression.sln" 49 | 50 | // Pattern specifying assemblies to be tested using NUnit 51 | let testAssemblies = "tests/**/bin/Release/*Tests*.dll" 52 | 53 | // Git configuration (used for publishing documentation in gh-pages branch) 54 | // The profile where the project is posted 55 | let gitOwner = "Thorium" 56 | let gitHome = "https://github.com/" + gitOwner 57 | 58 | // The name of the project on GitHub 59 | let gitName = "Owin.Compression" 60 | 61 | // The url for the raw files hosted 62 | let gitRaw = environVarOrDefault "gitRaw" "https://raw.github.com/Thorium" 63 | 64 | // -------------------------------------------------------------------------------------- 65 | // END TODO: The rest of the file includes standard build steps 66 | // -------------------------------------------------------------------------------------- 67 | 68 | // Read additional information from the release notes document 69 | let release = LoadReleaseNotes "RELEASE_NOTES.md" 70 | 71 | // Helper active pattern for project types 72 | let (|Fsproj|Csproj|Vbproj|Shproj|) (projFileName:string) = 73 | match projFileName with 74 | | f when f.EndsWith("fsproj") -> Fsproj 75 | | f when f.EndsWith("csproj") -> Csproj 76 | | f when f.EndsWith("vbproj") -> Vbproj 77 | | f when f.EndsWith("shproj") -> Shproj 78 | | _ -> failwith (sprintf "Project file %s not supported. Unknown project type." projFileName) 79 | 80 | // Generate assembly info files with the right version & up-to-date information 81 | Target "AssemblyInfo" (fun _ -> 82 | let getAssemblyInfoAttributes projectName = 83 | [ Attribute.Title (projectName) 84 | Attribute.Product project 85 | Attribute.Description summary 86 | Attribute.Version release.AssemblyVersion 87 | Attribute.FileVersion release.AssemblyVersion ] 88 | 89 | let getProjectDetails projectPath = 90 | let projectName = System.IO.Path.GetFileNameWithoutExtension(projectPath) 91 | ( projectPath, 92 | projectName, 93 | System.IO.Path.GetDirectoryName(projectPath), 94 | (getAssemblyInfoAttributes projectName) 95 | ) 96 | 97 | !! "src/**/*.??proj" 98 | |> Seq.map getProjectDetails 99 | |> Seq.iter (fun (projFileName, projectName, folderName, attributes) -> 100 | match projFileName with 101 | | Fsproj -> CreateFSharpAssemblyInfo (folderName > "AssemblyInfo.fs") attributes 102 | | Csproj -> CreateCSharpAssemblyInfo ((folderName > "Properties") > "AssemblyInfo.cs") attributes 103 | | Vbproj -> CreateVisualBasicAssemblyInfo ((folderName > "My Project") > "AssemblyInfo.vb") attributes 104 | | Shproj -> () 105 | ) 106 | ) 107 | 108 | 109 | // -------------------------------------------------------------------------------------- 110 | // Clean build results 111 | 112 | Target "Clean" (fun _ -> 113 | CleanDirs ["bin"; "temp"; "obj"] 114 | ) 115 | 116 | Target "CleanDocs" (fun _ -> 117 | CleanDirs ["docs/output"] 118 | ) 119 | 120 | // -------------------------------------------------------------------------------------- 121 | // Build library & test project 122 | 123 | Target "Build" (fun _ -> 124 | 125 | DotNetCli.Restore(fun p -> 126 | { p with 127 | Project = "Owin.Compression.sln" 128 | NoCache = true}) 129 | 130 | DotNetCli.Build(fun p -> 131 | { p with 132 | Project = "Owin.Compression.sln" 133 | Configuration = "Release"}) 134 | ) 135 | 136 | Target "BuildCore" (fun _ -> 137 | // Build .NET Core solution 138 | if not isMono then // Mono dotnet build is not tested yet... 139 | DotNetCli.Restore(fun p -> 140 | { p with 141 | Project = "src\Owin.Compression.Standard\Owin.Compression.Standard.fsproj" 142 | NoCache = true}) 143 | 144 | DotNetCli.Build(fun p -> 145 | { p with 146 | Project = "src\Owin.Compression.Standard\Owin.Compression.Standard.fsproj" 147 | Configuration = "Release"}) 148 | 149 | ) 150 | // -------------------------------------------------------------------------------------- 151 | // Run the unit tests using test runner 152 | 153 | Target "RunTests" (fun _ -> 154 | !! testAssemblies 155 | |> xUnit (fun p -> p) 156 | ) 157 | 158 | #if MONO 159 | #else 160 | // -------------------------------------------------------------------------------------- 161 | // SourceLink allows Source Indexing on the PDB generated by the compiler, this allows 162 | // the ability to step through the source code of external libraries http://ctaggart.github.io/SourceLink/ 163 | 164 | Target "SourceLink" (fun _ -> 165 | let baseUrl = sprintf "%s/%s/{0}/%%var2%%" gitRaw project 166 | !! "src/**/*.??proj" 167 | -- "src/**/*.shproj" 168 | |> Seq.iter (fun projFile -> 169 | let proj = VsProj.LoadRelease projFile 170 | SourceLink.Index proj.CompilesNotLinked proj.OutputFilePdb __SOURCE_DIRECTORY__ baseUrl 171 | ) 172 | ) 173 | 174 | #endif 175 | 176 | // -------------------------------------------------------------------------------------- 177 | // Build a NuGet package 178 | 179 | Target "NuGet" (fun _ -> 180 | Paket.Pack(fun p -> 181 | { p with 182 | OutputPath = "bin" 183 | Version = release.NugetVersion 184 | ReleaseNotes = toLines release.Notes}) 185 | ) 186 | 187 | Target "PublishNuget" (fun _ -> 188 | Paket.Push(fun p -> 189 | { p with 190 | WorkingDir = "bin" }) 191 | ) 192 | 193 | 194 | // -------------------------------------------------------------------------------------- 195 | // Generate the documentation 196 | let propsOverride = 197 | " fsdocs-logo-src https://github.com/Thorium/Owin.Compression/raw/master/docs/files/img/logo.png" + 198 | " fsdocs-navbar-position fixed-right " + 199 | " fsdocs-repository-link https://github.com/Thorium/Owin.Compression" + 200 | " fsdocs-list-of-namespaces -" 201 | 202 | 203 | Target "GenerateDocs" (fun _ -> 204 | CleanDirs [".fsdocs"] 205 | DotNetCli.RunCommand id ("fsdocs build --output docs/output --input docs/content --noapidocs --clean --parameters " + propsOverride) 206 | ) 207 | 208 | Target "WatchLocalDocs" (fun _ -> 209 | CleanDirs [".fsdocs"] 210 | DotNetCli.RunCommand id ("fsdocs watch --output docs/output --input docs/content --noapidocs --clean --parameters fsdocs-package-project-url http://localhost:8901/ " + propsOverride) |> ignore 211 | 212 | ) 213 | 214 | // -------------------------------------------------------------------------------------- 215 | // Release Scripts 216 | 217 | Target "ReleaseDocs" (fun _ -> 218 | let tempDocsDir = "temp/gh-pages" 219 | CleanDir tempDocsDir 220 | Repository.cloneSingleBranch "" (gitHome + "/" + gitName + ".git") "gh-pages" tempDocsDir 221 | 222 | CopyRecursive "docs/output" tempDocsDir true |> tracefn "%A" 223 | StageAll tempDocsDir 224 | Git.Commit.Commit tempDocsDir (sprintf "Update generated documentation for version %s" release.NugetVersion) 225 | Branches.push tempDocsDir 226 | ) 227 | 228 | #load "paket-files/build/fsharp/FAKE/modules/Octokit/Octokit.fsx" 229 | open Octokit 230 | 231 | Target "Release" (fun _ -> 232 | let user = 233 | match getBuildParam "github-user" with 234 | | s when not (String.IsNullOrWhiteSpace s) -> s 235 | | _ -> getUserInput "Username: " 236 | let pw = 237 | match getBuildParam "github-pw" with 238 | | s when not (String.IsNullOrWhiteSpace s) -> s 239 | | _ -> getUserPassword "Password: " 240 | let remote = 241 | Git.CommandHelper.getGitResult "" "remote -v" 242 | |> Seq.filter (fun (s: string) -> s.EndsWith("(push)")) 243 | |> Seq.tryFind (fun (s: string) -> s.Contains(gitOwner + "/" + gitName)) 244 | |> function None -> gitHome + "/" + gitName | Some (s: string) -> s.Split().[0] 245 | 246 | StageAll "" 247 | Git.Commit.Commit "" (sprintf "Bump version to %s" release.NugetVersion) 248 | Branches.pushBranch "" remote (Information.getBranchName "") 249 | 250 | Branches.tag "" release.NugetVersion 251 | Branches.pushTag "" remote release.NugetVersion 252 | 253 | // release on github 254 | createClient user pw 255 | |> createDraft gitOwner gitName release.NugetVersion (release.SemVer.PreRelease <> None) release.Notes 256 | // TODO: |> uploadFile "PATH_TO_FILE" 257 | |> releaseDraft 258 | |> Async.RunSynchronously 259 | ) 260 | 261 | Target "BuildPackage" DoNothing 262 | 263 | // -------------------------------------------------------------------------------------- 264 | // Run all targets by default. Invoke 'build ' to override 265 | 266 | Target "All" DoNothing 267 | 268 | "Clean" 269 | ==> "AssemblyInfo" 270 | ==> "Build" 271 | ==> "BuildCore" 272 | ==> "RunTests" 273 | ==> "GenerateDocs" 274 | ==> "All" 275 | =?> ("ReleaseDocs",isLocalBuild) 276 | 277 | "All" 278 | #if MONO 279 | #else 280 | //=?> ("SourceLink", Pdbstr.tryFind().IsSome ) 281 | #endif 282 | ==> "NuGet" 283 | ==> "BuildPackage" 284 | 285 | "CleanDocs" 286 | //==> "GenerateHelp" 287 | //==> "GenerateReferenceDocs" 288 | ==> "GenerateDocs" 289 | 290 | "ReleaseDocs" 291 | ==> "Release" 292 | 293 | "BuildPackage" 294 | ==> "PublishNuget" 295 | ==> "Release" 296 | 297 | // Use this to test and run document generation in localhost: 298 | // build WatchLocalDocs 299 | "BuildCore" 300 | ==> "WatchLocalDocs" 301 | 302 | RunTargetOrDefault "All" 303 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | cd `dirname $0` 7 | 8 | PAKET_BOOTSTRAPPER_EXE=.paket/paket.bootstrapper.exe 9 | PAKET_EXE=.paket/paket.exe 10 | FAKE_EXE=packages/build/FAKE/tools/FAKE.exe 11 | 12 | FSIARGS="" 13 | OS=${OS:-"unknown"} 14 | if [[ "$OS" != "Windows_NT" ]] 15 | then 16 | FSIARGS="--fsiargs -d:MONO" 17 | fi 18 | 19 | function run() { 20 | if [[ "$OS" != "Windows_NT" ]] 21 | then 22 | mono "$@" 23 | else 24 | "$@" 25 | fi 26 | } 27 | 28 | function yesno() { 29 | # NOTE: Defaults to NO 30 | read -p "$1 [y/N] " ynresult 31 | case "$ynresult" in 32 | [yY]*) true ;; 33 | *) false ;; 34 | esac 35 | } 36 | 37 | set +e 38 | run $PAKET_BOOTSTRAPPER_EXE 39 | bootstrapper_exitcode=$? 40 | set -e 41 | 42 | 43 | if [[ "$OS" != "Windows_NT" ]] && 44 | [ $bootstrapper_exitcode -ne 0 ] && 45 | [ $(certmgr -list -c Trust | grep X.509 | wc -l) -le 1 ] && 46 | [ $(certmgr -list -c -m Trust | grep X.509 | wc -l) -le 1 ] 47 | then 48 | echo "Your Mono installation has no trusted SSL root certificates set up." 49 | echo "This may result in the Paket bootstrapper failing to download Paket" 50 | echo "because Github's SSL certificate can't be verified. One way to fix" 51 | echo "this issue would be to download the list of SSL root certificates" 52 | echo "from the Mozilla project by running the following command:" 53 | echo "" 54 | echo " mozroots --import --sync" 55 | echo "" 56 | echo "This will import over 100 SSL root certificates into your Mono" 57 | echo "certificate repository." 58 | echo "" 59 | if yesno "Run 'mozroots --import --sync' now?" 60 | then 61 | mozroots --import --sync 62 | else 63 | echo "Attempting to continue without running mozroots. This might fail." 64 | fi 65 | # Re-run bootstrapper whether or not the user ran mozroots, because maybe 66 | # they fixed the problem in a separate terminal window. 67 | run $PAKET_BOOTSTRAPPER_EXE 68 | 69 | fi 70 | 71 | run $PAKET_EXE restore 72 | 73 | [ ! -e build.fsx ] && run $PAKET_EXE update 74 | [ ! -e build.fsx ] && run $FAKE_EXE init.fsx 75 | run $FAKE_EXE "$@" $FSIARGS build.fsx 76 | 77 | -------------------------------------------------------------------------------- /docs/content/content/fsdocs-custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://raw.githubusercontent.com/tonsky/FiraCode/fixed/distr/fira_code.css'); 2 | @import url('https://fonts.googleapis.com/css2?family=Hind+Vadodara&family=Roboto+Mono&display=swap'); 3 | /*-------------------------------------------------------------------------- 4 | Formatting for page & standard document content 5 | /*--------------------------------------------------------------------------*/ 6 | 7 | body { 8 | font-family: 'Hind Vadodara', sans-serif; 9 | /* padding-top: 0px; 10 | padding-bottom: 40px; 11 | */ 12 | } 13 | 14 | blockquote { 15 | margin: 0 1em 0 0.25em; 16 | margin-top: 0px; 17 | margin-right: 1em; 18 | margin-bottom: 0px; 19 | margin-left: 0.25em; 20 | padding: 0 .75em 0 1em; 21 | border-left: 1px solid #777; 22 | border-right: 0px solid #777; 23 | } 24 | 25 | /* Format the heading - nicer spacing etc. */ 26 | .masthead { 27 | overflow: hidden; 28 | } 29 | 30 | .masthead .muted a { 31 | text-decoration: none; 32 | color: #999999; 33 | } 34 | 35 | .masthead ul, .masthead li { 36 | margin-bottom: 0px; 37 | } 38 | 39 | .masthead .nav li { 40 | margin-top: 15px; 41 | font-size: 110%; 42 | } 43 | 44 | .masthead h3 { 45 | margin-top: 15px; 46 | margin-bottom: 5px; 47 | font-size: 170%; 48 | } 49 | 50 | /*-------------------------------------------------------------------------- 51 | Formatting fsdocs-content 52 | /*--------------------------------------------------------------------------*/ 53 | 54 | /* Change font sizes for headings etc. */ 55 | #fsdocs-content h1 { 56 | margin: 30px 0px 15px 0px; 57 | /* font-weight: 400; */ 58 | font-size: 2rem; 59 | letter-spacing: 1.78px; 60 | line-height: 2.5rem; 61 | font-weight: 400; 62 | } 63 | 64 | #fsdocs-content h2 { 65 | font-size: 1.6rem; 66 | margin: 20px 0px 10px 0px; 67 | font-weight: 400; 68 | } 69 | 70 | #fsdocs-content h3 { 71 | font-size: 1.2rem; 72 | margin: 15px 0px 10px 0px; 73 | font-weight: 400; 74 | } 75 | 76 | #fsdocs-content hr { 77 | margin: 0px 0px 20px 0px; 78 | } 79 | 80 | #fsdocs-content li { 81 | font-size: 1.0rem; 82 | line-height: 1.375rem; 83 | letter-spacing: 0.01px; 84 | font-weight: 500; 85 | margin: 0px 0px 15px 0px; 86 | } 87 | 88 | #fsdocs-content p { 89 | font-size: 1.0rem; 90 | line-height: 1.375rem; 91 | letter-spacing: 0.01px; 92 | font-weight: 500; 93 | color: #262626; 94 | } 95 | 96 | #fsdocs-content a { 97 | color: #4974D1; 98 | } 99 | /* remove the default bootstrap bold on dt elements */ 100 | #fsdocs-content dt { 101 | font-weight: normal; 102 | } 103 | 104 | 105 | 106 | /*-------------------------------------------------------------------------- 107 | Formatting tables in fsdocs-content, using docs.microsoft.com tables 108 | /*--------------------------------------------------------------------------*/ 109 | 110 | #fsdocs-content .table { 111 | table-layout: auto; 112 | width: 100%; 113 | font-size: 0.875rem; 114 | } 115 | 116 | #fsdocs-content .table caption { 117 | font-size: 0.8rem; 118 | font-weight: 600; 119 | letter-spacing: 2px; 120 | text-transform: uppercase; 121 | padding: 1.125rem; 122 | border-width: 0 0 1px; 123 | border-style: solid; 124 | border-color: #e3e3e3; 125 | text-align: right; 126 | } 127 | 128 | #fsdocs-content .table td, 129 | #fsdocs-content .table th { 130 | display: table-cell; 131 | word-wrap: break-word; 132 | padding: 0.75rem 1rem 0.75rem 0rem; 133 | line-height: 1.5; 134 | vertical-align: top; 135 | border-top: 1px solid #e3e3e3; 136 | border-right: 0; 137 | border-left: 0; 138 | border-bottom: 0; 139 | border-style: solid; 140 | } 141 | 142 | /* suppress the top line on inner lists such as tables of exceptions */ 143 | #fsdocs-content .table .fsdocs-exception-list td, 144 | #fsdocs-content .table .fsdocs-exception-list th { 145 | border-top: 0 146 | } 147 | 148 | #fsdocs-content .table td p:first-child, 149 | #fsdocs-content .table th p:first-child { 150 | margin-top: 0; 151 | } 152 | 153 | #fsdocs-content .table td.nowrap, 154 | #fsdocs-content .table th.nowrap { 155 | white-space: nowrap; 156 | } 157 | 158 | #fsdocs-content .table td.is-narrow, 159 | #fsdocs-content .table th.is-narrow { 160 | width: 15%; 161 | } 162 | 163 | #fsdocs-content .table th:not([scope='row']) { 164 | border-top: 0; 165 | border-bottom: 1px; 166 | } 167 | 168 | #fsdocs-content .table > caption + thead > tr:first-child > td, 169 | #fsdocs-content .table > colgroup + thead > tr:first-child > td, 170 | #fsdocs-content .table > thead:first-child > tr:first-child > td { 171 | border-top: 0; 172 | } 173 | 174 | #fsdocs-content .table table-striped > tbody > tr:nth-of-type(odd) { 175 | background-color: var(--box-shadow-light); 176 | } 177 | 178 | #fsdocs-content .table.min { 179 | width: unset; 180 | } 181 | 182 | #fsdocs-content .table.is-left-aligned td:first-child, 183 | #fsdocs-content .table.is-left-aligned th:first-child { 184 | padding-left: 0; 185 | } 186 | 187 | #fsdocs-content .table.is-left-aligned td:first-child a, 188 | #fsdocs-content .table.is-left-aligned th:first-child a { 189 | outline-offset: -0.125rem; 190 | } 191 | 192 | @media screen and (max-width: 767px), screen and (min-resolution: 120dpi) and (max-width: 767.9px) { 193 | #fsdocs-content .table.is-stacked-mobile td:nth-child(1) { 194 | display: block; 195 | width: 100%; 196 | padding: 1rem 0; 197 | } 198 | 199 | #fsdocs-content .table.is-stacked-mobile td:not(:nth-child(1)) { 200 | display: block; 201 | border-width: 0; 202 | padding: 0 0 1rem; 203 | } 204 | } 205 | 206 | #fsdocs-content .table.has-inner-borders th, 207 | #fsdocs-content .table.has-inner-borders td { 208 | border-right: 1px solid #e3e3e3; 209 | } 210 | 211 | #fsdocs-content .table.has-inner-borders th:last-child, 212 | #fsdocs-content .table.has-inner-borders td:last-child { 213 | border-right: none; 214 | } 215 | 216 | .fsdocs-entity-list .fsdocs-entity-name { 217 | width: 25%; 218 | font-weight: bold; 219 | } 220 | 221 | .fsdocs-member-list .fsdocs-member-usage { 222 | width: 35%; 223 | } 224 | 225 | /*-------------------------------------------------------------------------- 226 | Formatting xmldoc sections in fsdocs-content 227 | /*--------------------------------------------------------------------------*/ 228 | 229 | .fsdocs-xmldoc, .fsdocs-entity-xmldoc, .fsdocs-member-xmldoc { 230 | font-size: 1.0rem; 231 | line-height: 1.375rem; 232 | letter-spacing: 0.01px; 233 | font-weight: 500; 234 | color: #262626; 235 | } 236 | 237 | .fsdocs-xmldoc h1 { 238 | font-size: 1.2rem; 239 | margin: 10px 0px 0px 0px; 240 | } 241 | 242 | .fsdocs-xmldoc h2 { 243 | font-size: 1.2rem; 244 | margin: 10px 0px 0px 0px; 245 | } 246 | 247 | .fsdocs-xmldoc h3 { 248 | font-size: 1.1rem; 249 | margin: 10px 0px 0px 0px; 250 | } 251 | 252 | /* #fsdocs-nav .searchbox { 253 | margin-top: 30px; 254 | margin-bottom: 30px; 255 | } */ 256 | 257 | #fsdocs-nav img.logo{ 258 | width:90%; 259 | /* height:140px; */ 260 | /* margin:10px 0px 0px 20px; */ 261 | margin-top:40px; 262 | border-style:none; 263 | } 264 | 265 | #fsdocs-nav input{ 266 | /* margin-left: 20px; */ 267 | margin-right: 20px; 268 | margin-top: 20px; 269 | margin-bottom: 20px; 270 | width: 93%; 271 | -webkit-border-radius: 0; 272 | border-radius: 0; 273 | } 274 | 275 | #fsdocs-nav { 276 | /* margin-left: -5px; */ 277 | /* width: 90%; */ 278 | font-size:0.95rem; 279 | } 280 | 281 | #fsdocs-nav li.nav-header{ 282 | /* margin-left: -5px; */ 283 | /* width: 90%; */ 284 | padding-left: 0; 285 | color: #262626; 286 | text-transform: none; 287 | font-size:16px; 288 | margin-top: 9px; 289 | font-weight: bold; 290 | } 291 | 292 | #fsdocs-nav a{ 293 | padding-left: 0; 294 | color: #6c6c6d; 295 | /* margin-left: 5px; */ 296 | /* width: 90%; */ 297 | } 298 | 299 | /*-------------------------------------------------------------------------- 300 | Formatting pre and code sections in fsdocs-content (code highlighting is 301 | further below) 302 | /*--------------------------------------------------------------------------*/ 303 | 304 | #fsdocs-content code { 305 | /* font-size: 0.83rem; */ 306 | font: 0.85rem 'Fira Code Fixed', monospace; 307 | background-color: #f7f7f900; 308 | border: 0px; 309 | padding: 0px; 310 | /* word-wrap: break-word; */ 311 | /* white-space: pre; */ 312 | } 313 | 314 | /* omitted */ 315 | #fsdocs-content span.omitted { 316 | background: #3c4e52; 317 | border-radius: 5px; 318 | color: #808080; 319 | padding: 0px 0px 1px 0px; 320 | } 321 | 322 | #fsdocs-content pre .fssnip code { 323 | font: 0.86rem 'Fira Code Fixed', monospace; 324 | } 325 | 326 | #fsdocs-content table.pre, 327 | #fsdocs-content pre.fssnip, 328 | #fsdocs-content pre { 329 | line-height: 13pt; 330 | border: 0px solid #000000; 331 | border-top: 0px solid #070707; 332 | border-collapse: separate; 333 | white-space: pre; 334 | font: 0.90rem 'Fira Code Fixed', monospace; 335 | width: 100%; 336 | margin: 10px 0px 20px 0px; 337 | background-color: #1E1E1E; 338 | padding: 10px; 339 | border-radius: 5px; 340 | color: #e2e2e2; 341 | max-width: none; 342 | box-sizing: border-box; 343 | font-weight: lighter; 344 | } 345 | 346 | #fsdocs-content pre.fssnip code { 347 | font: 0.86rem 'Fira Code Fixed', monospace; 348 | font-weight: 600; 349 | } 350 | 351 | #fsdocs-content table.pre { 352 | background-color: #0a0a0a; 353 | } 354 | 355 | #fsdocs-content table.pre pre { 356 | padding: 0px; 357 | margin: 0px; 358 | border-radius: 0px; 359 | width: 100%; 360 | background-color: #1d1d1d; 361 | color: #c9c9c9; 362 | } 363 | 364 | #fsdocs-content table.pre td { 365 | padding: 0px; 366 | white-space: normal; 367 | margin: 0px; 368 | width: 100%; 369 | } 370 | 371 | #fsdocs-content table.pre td.lines { 372 | width: 30px; 373 | } 374 | 375 | 376 | #fsdocs-content pre { 377 | word-wrap: inherit; 378 | } 379 | 380 | .fsdocs-example-header { 381 | font-size: 1.0rem; 382 | line-height: 1.375rem; 383 | letter-spacing: 0.01px; 384 | font-weight: 700; 385 | color: #262626; 386 | } 387 | 388 | /*-------------------------------------------------------------------------- 389 | Formatting github source links 390 | /*--------------------------------------------------------------------------*/ 391 | 392 | .fsdocs-source-link { 393 | float: right; 394 | text-decoration: none; 395 | } 396 | 397 | .fsdocs-source-link img { 398 | border-style: none; 399 | margin-left: 10px; 400 | width: auto; 401 | height: 1.4em; 402 | } 403 | 404 | .fsdocs-source-link .hover { 405 | display: none; 406 | } 407 | 408 | .fsdocs-source-link:hover .hover { 409 | display: block; 410 | } 411 | 412 | .fsdocs-source-link .normal { 413 | display: block; 414 | } 415 | 416 | .fsdocs-source-link:hover .normal { 417 | display: none; 418 | } 419 | 420 | /*-------------------------------------------------------------------------- 421 | Formatting logo 422 | /*--------------------------------------------------------------------------*/ 423 | 424 | #fsdocs-logo { 425 | width:140px; 426 | height:140px; 427 | margin:10px 0px 0px 0px; 428 | border-style:none; 429 | } 430 | 431 | /*-------------------------------------------------------------------------- 432 | 433 | /*--------------------------------------------------------------------------*/ 434 | 435 | #fsdocs-content table.pre pre { 436 | padding: 0px; 437 | margin: 0px; 438 | border: none; 439 | } 440 | 441 | /*-------------------------------------------------------------------------- 442 | Remove formatting from links 443 | /*--------------------------------------------------------------------------*/ 444 | 445 | #fsdocs-content h1 a, 446 | #fsdocs-content h1 a:hover, 447 | #fsdocs-content h1 a:focus, 448 | #fsdocs-content h2 a, 449 | #fsdocs-content h2 a:hover, 450 | #fsdocs-content h2 a:focus, 451 | #fsdocs-content h3 a, 452 | #fsdocs-content h3 a:hover, 453 | #fsdocs-content h3 a:focus, 454 | #fsdocs-content h4 a, 455 | #fsdocs-content h4 a:hover, #fsdocs-content 456 | #fsdocs-content h4 a:focus, 457 | #fsdocs-content h5 a, 458 | #fsdocs-content h5 a:hover, 459 | #fsdocs-content h5 a:focus, 460 | #fsdocs-content h6 a, 461 | #fsdocs-content h6 a:hover, 462 | #fsdocs-content h6 a:focus { 463 | color: #262626; 464 | text-decoration: none; 465 | text-decoration-style: none; 466 | /* outline: none */ 467 | } 468 | 469 | /*-------------------------------------------------------------------------- 470 | Formatting for F# code snippets 471 | /*--------------------------------------------------------------------------*/ 472 | 473 | .fsdocs-param-name, 474 | .fsdocs-return-name, 475 | .fsdocs-param { 476 | font-weight: 900; 477 | font-size: 0.90rem; 478 | font-family: 'Fira Code Fixed', monospace; 479 | } 480 | /* strings --- and stlyes for other string related formats */ 481 | #fsdocs-content span.en { 482 | color: #adaf69; 483 | } 484 | #fsdocs-content span.s { 485 | color: #ea9a75; 486 | } 487 | /* printf formatters */ 488 | #fsdocs-content span.pf { 489 | color: #E0C57F; 490 | } 491 | /* escaped chars */ 492 | #fsdocs-content span.e { 493 | color: #EA8675; 494 | } 495 | 496 | /* identifiers --- and styles for more specific identifier types */ 497 | #fsdocs-content span.id { 498 | color: #d1d1d1; 499 | } 500 | /* module */ 501 | #fsdocs-content span.m { 502 | color: #43AEC6; 503 | } 504 | /* reference type */ 505 | #fsdocs-content span.rt { 506 | color: #6a8dd8; 507 | } 508 | /* value type */ 509 | #fsdocs-content span.vt { 510 | color: #43AEC6; 511 | } 512 | /* interface */ 513 | #fsdocs-content span.if { 514 | color: #43AEC6; 515 | } 516 | /* type argument */ 517 | #fsdocs-content span.ta { 518 | color: #43AEC6; 519 | } 520 | /* disposable */ 521 | #fsdocs-content span.d { 522 | color: #2f798a; 523 | } 524 | /* property */ 525 | #fsdocs-content span.prop { 526 | color: #43AEC6; 527 | } 528 | /* punctuation */ 529 | #fsdocs-content span.p { 530 | color: #43AEC6; 531 | } 532 | #fsdocs-content span.pn { 533 | color: #e1e1e1; 534 | } 535 | /* function */ 536 | #fsdocs-content span.f { 537 | color: #e1e1e1; 538 | } 539 | #fsdocs-content span.fn { 540 | color: #43AEC6; 541 | } 542 | /* active pattern */ 543 | #fsdocs-content span.pat { 544 | color: #4ec9b0; 545 | } 546 | /* union case */ 547 | #fsdocs-content span.u { 548 | color: #4ec9b0; 549 | } 550 | /* enumeration */ 551 | #fsdocs-content span.e { 552 | color: #4ec9b0; 553 | } 554 | /* keywords */ 555 | #fsdocs-content span.k { 556 | color: #2248c4; 557 | } 558 | /* comment */ 559 | #fsdocs-content span.c { 560 | color: #329215; 561 | font-weight: 400; 562 | font-style: italic; 563 | } 564 | /* operators */ 565 | #fsdocs-content span.o { 566 | color: #af75c1; 567 | } 568 | /* numbers */ 569 | #fsdocs-content span.n { 570 | color: #96C71D; 571 | } 572 | /* line number */ 573 | #fsdocs-content span.l { 574 | color: #80b0b0; 575 | } 576 | /* mutable var or ref cell */ 577 | #fsdocs-content span.v { 578 | color: #997f0c; 579 | font-weight: bold; 580 | } 581 | /* inactive code */ 582 | #fsdocs-content span.inactive { 583 | color: #808080; 584 | } 585 | /* preprocessor */ 586 | #fsdocs-content span.prep { 587 | color: #af75c1; 588 | } 589 | /* fsi output */ 590 | #fsdocs-content span.fsi { 591 | color: #808080; 592 | } 593 | 594 | /* tool tip */ 595 | div.fsdocs-tip { 596 | background: #475b5f; 597 | border-radius: 4px; 598 | font: 0.85rem 'Fira Code Fixed', monospace; 599 | padding: 6px 8px 6px 8px; 600 | display: none; 601 | color: #d1d1d1; 602 | pointer-events: none; 603 | } 604 | 605 | div.fsdocs-tip code { 606 | color: #d1d1d1; 607 | font: 0.85rem 'Fira Code Fixed', monospace; 608 | } 609 | 610 | -------------------------------------------------------------------------------- /docs/content/index.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | // This block of code is omitted in the generated HTML documentation. Use 3 | // it to define helpers that you do not want to show in the documentation. 4 | #I "../../bin" 5 | #I @"./../../packages/Owin/lib/net40" 6 | #r @"nuget: Microsoft.Owin" 7 | #r @"nuget: Microsoft.Owin.Hosting" 8 | #r @"nuget: Microsoft.Owin.Host.HttpListener" 9 | #r @"nuget: Owin.Compression" 10 | #r @"nuget: Microsoft.Owin.StaticFiles" 11 | #r @"nuget: Microsoft.Owin.FileSystems" 12 | 13 | (** 14 | Owin.Compression 15 | ====================== 16 | 17 | Owin.Compression (Deflate / GZip) module ("middleware") for the Microsoft OWIN pipeline. It can be used with .NET Full, .NET Core, .NET Standard, .NET6.0, and so on. It also works with Selfhost and AspNetCore (e.g. with Kestrel, which is OWIN based server). 18 | It compresses the web request responses to make the transfer smaller, and it supports eTag caching. 19 | 20 | 21 | 22 | 23 | 24 | The Owin.Compression library can be installed from NuGet: 25 | PM> Install-Package Owin.Compression 26 | 27 | 28 | 29 | 30 | 31 | The default compression used is deflate, then gzip, as deflate should be faster. 32 | This also supports streaming responses. The config allows you to disable deflate and streaming if you prefer. 33 | 34 | 35 | eTag-caching 36 | ---------- 37 | 38 | 1. When the server reads the content before compression, it calculates a hash-code over it. 39 | 2. The hash-code is sent as ETag response header to the client with the response 40 | 3. The next time the client asks for the same resource, it sends an If-None-Match header in the request with the same value. 41 | 4. After the server reads the content before the compression, it calculates a hash-code over it. If it matches the If-None-Match of the request, the server can skip the compression and skip the sending and just send http status code 304 to the client which means "use what you have, it's not modified since". 42 | 43 | 44 | Example #1 45 | ---------- 46 | 47 | This example demonstrates using MapCompressionModule-function defined in this sample library. 48 | 49 | ```csharp 50 | using System; 51 | using Owin; 52 | [assembly: Microsoft.Owin.OwinStartup(typeof(MyServer.MyWebStartup))] 53 | namespace MyServer 54 | { 55 | class MyWebStartup 56 | { 57 | public void Configuration(Owin.IAppBuilder app) 58 | { 59 | // This will compress the whole request, if you want to use e.g. Microsoft.Owin.StaticFiles server: 60 | // app.UseCompressionModule() 61 | 62 | var settings = OwinCompression.DefaultCompressionSettingsWithPath("c:\\temp\\"); //" server path 63 | //or var settings = new CompressionSettings( ... ) 64 | app.MapCompressionModule("/zipped", settings); 65 | } 66 | } 67 | 68 | class Program 69 | { 70 | static void Main(string[] args) 71 | { 72 | Microsoft.Owin.Hosting.WebApp.Start("http://*:8080"); //" run on localhost. 73 | Console.WriteLine("Server started... Press enter to exit."); 74 | Console.ReadLine(); 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | And now your files are smaller than with e.g. just Microsoft.Owin.StaticFiles -library server: 81 | 82 | 83 | 84 | Even though the browser sees everything as plain text, the traffic is actually transferred in compressed format. 85 | You can monitor the traffic with e.g. Fiddler. 86 | 87 | Example #2 88 | ---------- 89 | 90 | Running on OWIN Self-Host (Microsoft.Owin.Hosting) with static files server (Microsoft.Owin.StaticFiles) 91 | and compressing only the ".json"-responses (and files) on-the-fly, with only gzip and not deflate: 92 | 93 | ```csharp 94 | using System; 95 | using Owin; 96 | [assembly: Microsoft.Owin.OwinStartup(typeof(MyServer.MyWebStartup))] 97 | namespace MyServer 98 | { 99 | class MyWebStartup 100 | { 101 | public void Configuration(Owin.IAppBuilder app) 102 | { 103 | var settings = new CompressionSettings( 104 | serverPath: "", 105 | allowUnknonwnFiletypes: false, 106 | allowRootDirectories: false, 107 | cacheExpireTime: Microsoft.FSharp.Core.FSharpOption.None, 108 | allowedExtensionAndMimeTypes: 109 | new[] { Tuple.Create(".json", "application/json") }, 110 | minimumSizeToCompress: 1000, 111 | streamingDisabled: false, 112 | deflateDisabled: true 113 | ); 114 | app.UseCompressionModule(settings); 115 | } 116 | } 117 | 118 | class Program 119 | { 120 | static void Main(string[] args) 121 | { 122 | Microsoft.Owin.Hosting.WebApp.Start("http://*:8080"); 123 | Console.WriteLine("Server started... Press enter to exit."); 124 | Console.ReadLine(); 125 | } 126 | } 127 | } 128 | ``` 129 | 130 | Example #3 131 | ---------- 132 | 133 | Running on OWIN Self-Host (Microsoft.Owin.Hosting) with static files server (Microsoft.Owin.StaticFiles) 134 | and compressing all the responses (and files) on-the-fly. This example is in F-Sharp (and can be run with F#-interactive): 135 | 136 | *) 137 | 138 | 139 | #r "Owin.dll" 140 | #r "Microsoft.Owin.dll" 141 | #r "Microsoft.Owin.FileSystems.dll" 142 | #r "Microsoft.Owin.Hosting.dll" 143 | #r "Microsoft.Owin.StaticFiles.dll" 144 | #r "System.Configuration.dll" 145 | #r "Owin.Compression.dll" 146 | 147 | open Owin 148 | open System 149 | 150 | module Examples = 151 | 152 | type MyStartup() = 153 | member __.Configuration(app:Owin.IAppBuilder) = 154 | let app1 = app.UseCompressionModule() 155 | app1.UseFileServer "/." |> ignore 156 | () 157 | 158 | let server = Microsoft.Owin.Hosting.WebApp.Start "http://*:6000" 159 | Console.WriteLine "Press Enter to stop & quit." 160 | Console.ReadLine() |> ignore 161 | server.Dispose() 162 | 163 | (** 164 | 165 | Example #4 166 | ---------- 167 | 168 | Running on ASP.NET Core web API on .NET 6.0. You can use C# but this example is in F# 169 | just because of the shorter syntax. The full project is available in tests-folder of this project: 170 | 171 | *) 172 | 173 | open System 174 | open Microsoft.AspNetCore.Builder 175 | open Microsoft.Extensions.DependencyInjection 176 | open Microsoft.Extensions.Hosting 177 | open Owin 178 | 179 | module Program = 180 | 181 | [] 182 | let main args = 183 | 184 | let builder = WebApplication.CreateBuilder args 185 | builder.Services.AddControllers() |> ignore 186 | let app = builder.Build() 187 | 188 | let compressionSetting = 189 | {OwinCompression.DefaultCompressionSettings with 190 | CacheExpireTime = Some (DateTimeOffset.Now.AddDays 7.) 191 | AllowUnknonwnFiletypes = true 192 | StreamingDisabled = true 193 | } 194 | (app :> IApplicationBuilder).UseCompressionModule(compressionSetting) |> ignore 195 | app.MapControllers() |> ignore 196 | app.Run() 197 | 0 198 | (** 199 | 200 | https://github.com/Thorium/Owin.Compression/tree/master/tests/Aspnet.Core.WebAPI.Test 201 | 202 | Example #5 203 | ---------- 204 | 205 | More complete examples can be found here. 206 | 207 | 208 | Samples & documentation 209 | ----------------------- 210 | 211 | The library comes with comprehensible documentation. 212 | It can include tutorials automatically generated from `*.fsx` files in [the content folder][content]. 213 | The API reference is automatically generated from Markdown comments in the library implementation. 214 | 215 | * [Tutorial](tutorial.html) contains a further explanation of this sample library. 216 | 217 | * [API Reference](reference/index.html) contains automatically generated documentation for all types, modules 218 | and functions in the library. This includes additional brief samples on using most of the 219 | functions. 220 | 221 | Contributing and copyright 222 | -------------------------- 223 | 224 | The project is hosted on [GitHub][gh] where you can [report issues][issues], fork 225 | the project and submit pull requests. If you're adding a new public API, please also 226 | consider adding [samples][content] that can be turned into documentation. You might 227 | also want to read the [library design notes][readme] to understand how it works. 228 | 229 | The library is available under a Public Domain license, which allows modification and 230 | redistribution for both commercial and non-commercial purposes. For more information see the 231 | [License file][license] in the GitHub repository. 232 | 233 | [content]: https://github.com/fsprojects/Owin.Compression/tree/master/docs/content 234 | [gh]: https://github.com/fsprojects/Owin.Compression 235 | [issues]: https://github.com/fsprojects/Owin.Compression/issues 236 | [readme]: https://github.com/fsprojects/Owin.Compression/blob/master/README.md 237 | [license]: https://github.com/fsprojects/Owin.Compression/blob/master/LICENSE.txt 238 | *) 239 | -------------------------------------------------------------------------------- /docs/content/tutorial.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | // This block of code is omitted in the generated HTML documentation. Use 3 | // it to define helpers that you do not want to show in the documentation. 4 | #I "../../bin" 5 | #I @"./../../packages/Owin/lib/net40" 6 | #I @"./../../packages/Microsoft.Owin/lib/net451" 7 | #I @"./../../packages/Microsoft.Owin.Hosting/lib/net451" 8 | #I @"./../../packages/Microsoft.Owin.Host.HttpListener/lib/net451" 9 | #I @"./../../bin/Owin.Compression" 10 | 11 | (** 12 | # Using this library (C-Sharp) # 13 | 14 | Create a new C# console application project (.NET 4.5 or more). Add reference to NuGet-packages: 15 | 16 | - Microsoft.Owin 17 | - Microsoft.Owin.Hosting 18 | - Microsoft.Owin.Host.HttpListener 19 | - Owin.Compression (this package) 20 | 21 | Then write the program, e.g.: 22 | 23 | ```csharp 24 | using System; 25 | using Owin; 26 | [assembly: Microsoft.Owin.OwinStartup(typeof(MyServer.MyWebStartup))] 27 | namespace MyServer 28 | { 29 | class MyWebStartup 30 | { 31 | public void Configuration(Owin.IAppBuilder app) 32 | { 33 | var settings = OwinCompression.DefaultCompressionSettingsWithPath(@"c:\temp\"); 34 | //or var settings = new CompressionSettings( ... ) 35 | app.MapCompressionModule("/zipped", settings); 36 | } 37 | } 38 | 39 | class Program 40 | { 41 | static void Main(string[] args) 42 | { 43 | Microsoft.Owin.Hosting.WebApp.Start("http://*:8080"); 44 | Console.WriteLine("Server started... Press enter to exit."); 45 | Console.ReadLine(); 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | Have a large text file in your temp-folder, c:\temp\test\mytempfile.txt 52 | 53 | Now, run the program (F5) and start a browser to address: 54 | 55 | http://localhost:8080/zipped/test/mytempfile.txt 56 | 57 | Observe that the file is transfered as compressed but the browser will automatically decompress the traffic. 58 | 59 | 60 | 61 | ### Corresponding code with F-Sharp ### 62 | 63 | *) 64 | #r "Owin.dll" 65 | #r "Microsoft.Owin.dll" 66 | #r "Microsoft.Owin.Hosting.dll" 67 | #r "System.Configuration.dll" 68 | #r "Owin.Compression.dll" 69 | 70 | open Owin 71 | open System 72 | 73 | let serverPath = System.Configuration.ConfigurationManager.AppSettings.["WwwRoot"] 74 | 75 | type MyWebStartup() = 76 | member __.Configuration(app:Owin.IAppBuilder) = 77 | let compressionSetting = 78 | {OwinCompression.DefaultCompressionSettings with 79 | ServerPath = serverPath; 80 | CacheExpireTime = Some (DateTimeOffset.Now.AddDays 7.) } 81 | app.MapCompressionModule("/zipped", compressionSetting) |> ignore 82 | () 83 | 84 | [)>] 85 | do() 86 | 87 | // and then... 88 | 89 | Microsoft.Owin.Hosting.WebApp.Start "http://*:8080" 90 | 91 | (** 92 | You can also use app.UseCompressionModule() at the beginning of the configuration to compress the whole response. 93 | *) 94 | 95 | type MyWebStartupExample2() = 96 | member __.Configuration(app:Owin.IAppBuilder) = 97 | app.UseCompressionModule() |> ignore 98 | 99 | //app.MapSignalR(hubConfig) 100 | //app.UseFileServer(fileServerOptions) |> ignore 101 | //etc... 102 | 103 | () 104 | -------------------------------------------------------------------------------- /docs/files/img/logo-template.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thorium/Owin.Compression/67fac5ce33b33f2538457538e69042dbd94233ed/docs/files/img/logo-template.pdn -------------------------------------------------------------------------------- /docs/files/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thorium/Owin.Compression/67fac5ce33b33f2538457538e69042dbd94233ed/docs/files/img/logo.png -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | This file is in the `lib` directory. 2 | 3 | Any **libraries** on which your project depends and which are **NOT managed via NuGet** should be kept **in this directory**. 4 | This typically includes custom builds of third-party software, private (i.e. to a company) codebases, and native libraries. 5 | 6 | --- 7 | NOTE: 8 | 9 | This file is a placeholder, used to preserve directory structure in Git. 10 | 11 | This file does not need to be edited. 12 | -------------------------------------------------------------------------------- /netfx.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | true 8 | 9 | 10 | /Library/Frameworks/Mono.framework/Versions/Current/lib/mono 11 | /usr/lib/mono 12 | /usr/local/lib/mono 13 | 14 | 15 | $(BaseFrameworkPathOverrideForMono)/4.5-api 16 | $(BaseFrameworkPathOverrideForMono)/4.5.1-api 17 | $(BaseFrameworkPathOverrideForMono)/4.5.2-api 18 | $(BaseFrameworkPathOverrideForMono)/4.6-api 19 | $(BaseFrameworkPathOverrideForMono)/4.6.1-api 20 | $(BaseFrameworkPathOverrideForMono)/4.6.2-api 21 | $(BaseFrameworkPathOverrideForMono)/4.7-api 22 | $(BaseFrameworkPathOverrideForMono)/4.7.1-api 23 | true 24 | 25 | 26 | $(FrameworkPathOverride)/Facades;$(AssemblySearchPaths) 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /nuget.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://nuget.org/api/v2 2 | 3 | nuget Owin >= 1.0 4 | nuget Microsoft.Owin 5 | nuget Microsoft.Owin.Hosting 6 | nuget Microsoft.Owin.Host.HttpListener 7 | nuget Microsoft.Owin.StaticFiles 8 | nuget FSharp.Core 9 | 10 | group standard 11 | source https://nuget.org/api/v2 12 | 13 | nuget Microsoft.AspNetCore.Http.Abstractions 14 | nuget Microsoft.AspNetCore.Http.Features 15 | nuget Microsoft.Extensions.Primitives 16 | 17 | // nuget Microsoft.AspNet.WebApi.OwinSelfHost 18 | // nuget Microsoft.Owin.StaticFiles 19 | 20 | group Build 21 | source https://nuget.org/api/v2 22 | 23 | nuget SourceLink.Fake 24 | nuget FAKE 25 | nuget FSharp.Formatting 26 | 27 | github fsharp/FAKE modules/Octokit/Octokit.fsx 28 | 29 | group Test 30 | source https://nuget.org/api/v2 31 | 32 | nuget BenchmarkDotNet 33 | nuget xunit.core 34 | nuget xunit.abstractions 35 | nuget xunit.runner.visualstudio >= 2.0 version_in_path: true 36 | nuget xunit.runner.console 37 | nuget FsUnit.xUnit 38 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thorium/Owin.Compression/67fac5ce33b33f2538457538e69042dbd94233ed/screen.png -------------------------------------------------------------------------------- /src/Owin.Compression.Standard/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | do () 11 | 12 | module internal AssemblyVersionInformation = 13 | let [] AssemblyTitle = "Owin.Compression.Standard" 14 | let [] AssemblyProduct = "Owin.Compression" 15 | let [] AssemblyDescription = "Compression (Deflate / GZip) module for Microsoft OWIN Selfhost filesystem pipeline." 16 | let [] AssemblyVersion = "1.0.46" 17 | let [] AssemblyFileVersion = "1.0.46" 18 | -------------------------------------------------------------------------------- /src/Owin.Compression.Standard/CompressionModule.fs: -------------------------------------------------------------------------------- 1 | #if INTERACTIVE 2 | #r "../../packages/standard/Microsoft.AspNetCore.Http.Abstractions/lib/netstandard2.0/Microsoft.AspNetCore.Http.Abstractions.dll" 3 | #r "../../packages/standard/Microsoft.AspNetCore.Http.Features/lib/netstandard2.0/Microsoft.AspNetCore.Http.Features.dll" 4 | #r "../../packages/standard/Microsoft.Extensions.Primitives/lib/netstandard2.0/Microsoft.Extensions.Primitives.dll" 5 | #r @"C:\Program Files\dotnet\sdk\2.0.0\Microsoft\Microsoft.NET.Build.Extensions\net461\lib\netstandard.dll" 6 | #else 7 | namespace Owin 8 | #endif 9 | 10 | open System 11 | open System.IO 12 | open System.IO.Compression 13 | open Microsoft.AspNetCore.Http 14 | open Microsoft.AspNetCore.Http.Abstractions 15 | open Microsoft.AspNetCore.Builder 16 | open Microsoft.Extensions.Primitives 17 | open System.Threading.Tasks 18 | open System.Runtime.CompilerServices 19 | open System.Collections.Generic 20 | 21 | /// Supported compression methods 22 | [] 23 | type SupportedEncodings = 24 | | Deflate 25 | | GZip 26 | 27 | /// Do you fetch files or do you encode context.Response.Body? 28 | type ResponseMode = 29 | | File 30 | | ContextResponseBody of Next: Func 31 | 32 | /// Settings for compression. 33 | type CompressionSettings = { 34 | ServerPath: string; 35 | AllowUnknonwnFiletypes: bool; 36 | AllowRootDirectories: bool; 37 | CacheExpireTime: DateTimeOffset voption; 38 | AllowedExtensionAndMimeTypes: IEnumerable; 39 | MinimumSizeToCompress: int64; 40 | DeflateDisabled: bool; 41 | StreamingDisabled: bool; 42 | ExcludedPaths: IEnumerable; 43 | } 44 | 45 | module OwinCompression = 46 | #if INTERACTIVE 47 | let basePath = __SOURCE_DIRECTORY__; 48 | #else 49 | let basePath = System.Reflection.Assembly.GetExecutingAssembly().Location |> Path.GetDirectoryName 50 | #endif 51 | /// Default settings for compression. 52 | let DefaultCompressionSettings = { 53 | ServerPath = basePath; 54 | AllowUnknonwnFiletypes = false; 55 | AllowRootDirectories = false; 56 | CacheExpireTime = ValueNone 57 | MinimumSizeToCompress = 1000L 58 | DeflateDisabled = false 59 | StreamingDisabled = false 60 | ExcludedPaths = [| "/signalr/" |] 61 | AllowedExtensionAndMimeTypes = 62 | [| 63 | ".js" , "application/javascript"; 64 | ".css" , "text/css"; 65 | ".yml" , "application/x-yaml"; 66 | ".json" , "application/json"; 67 | ".svg" , "image/svg+xml"; 68 | ".txt" , "text/plain"; 69 | ".html" , "application/json"; // we don't want to follow hyperlinks, so not "text/html" 70 | ".map" , "application/octet-stream"; 71 | ".ttf" , "application/x-font-ttf"; 72 | ".otf" , "application/x-font-opentype"; 73 | ".ico" , "image/x-icon"; 74 | ".map" , "application/json"; 75 | ".xml" , "application/xml"; 76 | ".xsl" , "text/xml"; 77 | ".xhtml", "application/xhtml+xml"; 78 | ".rss" , "application/rss+xml"; 79 | ".eot" , "font/eot"; 80 | ".aspx" , "text/html"; 81 | |] 82 | } 83 | 84 | /// Default settings with custom path. No cache time. 85 | let DefaultCompressionSettingsWithPath path = 86 | {DefaultCompressionSettings with 87 | ServerPath = path; CacheExpireTime = ValueSome (DateTimeOffset.Now.AddDays 7.) } 88 | 89 | /// Default settings with custom path and cache-time. C#-helper method. 90 | let DefaultCompressionSettingsWithPathAndCache(path,cachetime) = 91 | {DefaultCompressionSettings with ServerPath = path; CacheExpireTime = ValueSome (cachetime) } 92 | 93 | let private defaultBufferSize = 81920 94 | 95 | module Internals = 96 | 97 | let getHash (item:Stream) = 98 | if item.CanRead then 99 | let hasPos = 100 | if item.CanSeek && item.Position > 0L then 101 | let tmp = item.Position 102 | item.Position <- 0L 103 | ValueSome tmp 104 | else ValueNone 105 | use md5 = System.Security.Cryptography.MD5.Create() 106 | let res = BitConverter.ToString(md5.ComputeHash item).Replace("-","") 107 | match hasPos with 108 | | ValueSome x when item.CanSeek -> item.Position <- x 109 | | _ -> () 110 | ValueSome res 111 | else ValueNone 112 | 113 | let inline create304Response (contextResponse:HttpResponse) = 114 | if not contextResponse.HasStarted then 115 | if contextResponse.StatusCode <> 304 then 116 | contextResponse.StatusCode <- 304 117 | contextResponse.Body.Close() 118 | contextResponse.Body <- Stream.Null 119 | if contextResponse.ContentLength.HasValue then 120 | contextResponse.ContentLength <- Nullable() 121 | false 122 | 123 | let checkNoValidETag (contextRequest:HttpRequest) (contextResponse:HttpResponse) (cancellationSrc:Threading.CancellationTokenSource) (itemToCheck:Stream) = 124 | let ifNoneMatch, noneMatchValue = 125 | match contextRequest.Headers.TryGetValue "If-None-Match" with 126 | | true, nonem when nonem <> StringValues.Empty -> true, nonem 127 | | _ -> false, StringValues.Empty 128 | 129 | let hasNoPragma = 130 | match contextRequest.Headers.TryGetValue "Pragma" with 131 | | false, _ -> true 132 | | true, x when x <> StringValues("no-cache") -> true 133 | | true, _ -> false 134 | 135 | let etagVal = 136 | match contextResponse.Headers.TryGetValue "ETag" with 137 | | true, etag -> etag 138 | | false, _ -> StringValues.Empty 139 | if ifNoneMatch && hasNoPragma then 140 | if noneMatchValue = etagVal then 141 | if not (isNull cancellationSrc) then cancellationSrc.Cancel() 142 | create304Response contextResponse 143 | else 144 | 145 | match getHash itemToCheck with 146 | | ValueSome etag -> 147 | if noneMatchValue = StringValues(etag) then 148 | if not (isNull cancellationSrc) then cancellationSrc.Cancel() 149 | create304Response contextResponse 150 | else 151 | if etagVal = StringValues.Empty && 152 | not contextResponse.Headers.IsReadOnly then 153 | contextResponse.Headers.["ETag"] <- StringValues(etag) 154 | true 155 | | ValueNone -> true 156 | else 157 | if etagVal = StringValues.Empty then 158 | match getHash(itemToCheck) with 159 | | ValueSome etag when not contextResponse.Headers.IsReadOnly && itemToCheck.Length > 0L -> 160 | contextResponse.Headers.["ETag"] <- StringValues etag 161 | | _ -> () 162 | true 163 | 164 | let internal getFile (settings:CompressionSettings) (contextRequest:HttpRequest) (contextResponse:HttpResponse) (cancellationSrc:Threading.CancellationTokenSource) = 165 | let unpacked :string = 166 | let p = contextRequest.Path.ToString() 167 | if not(settings.AllowRootDirectories) && p.Contains ".." then failwith "Invalid path" 168 | if File.Exists p then failwith "Invalid resource" 169 | let p2 = 170 | #if NETSTANDARD21 171 | match p.StartsWith '/' with 172 | #else 173 | match p.StartsWith "/" with 174 | #endif 175 | | true -> p.Substring 1 176 | | false -> p 177 | Path.Combine ([| settings.ServerPath; p2|]) 178 | 179 | let extension = 180 | #if NETSTANDARD21 181 | if not (unpacked.Contains '.') then "" 182 | #else 183 | if not (unpacked.Contains ".") then "" 184 | #endif 185 | else unpacked.Substring(unpacked.LastIndexOf '.') 186 | let typemap = settings.AllowedExtensionAndMimeTypes |> Map.ofSeq 187 | 188 | match typemap.TryGetValue extension with 189 | | true, extval -> contextResponse.ContentType <- extval 190 | | false, _ when settings.AllowUnknonwnFiletypes -> () 191 | | _ -> 192 | if not contextResponse.HasStarted then 193 | contextResponse.StatusCode <- 415 194 | raise (ArgumentException("Invalid resource type", contextRequest.Path.ToString())) 195 | 196 | task { 197 | try 198 | use strm = File.OpenText unpacked 199 | let! txt = strm.ReadToEndAsync() 200 | let bytes = txt |> System.Text.Encoding.UTF8.GetBytes 201 | match FileInfo(unpacked).Length < settings.MinimumSizeToCompress with 202 | | true -> 203 | return false, bytes 204 | | false -> 205 | let lastmodified = File.GetLastWriteTimeUtc(unpacked).ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", System.Globalization.CultureInfo.InvariantCulture) 206 | contextResponse.Headers.Add("Last-Modified", StringValues(lastmodified)) 207 | return 208 | if checkNoValidETag contextRequest contextResponse cancellationSrc strm.BaseStream then 209 | true, bytes 210 | else 211 | false, null 212 | with 213 | | :? FileNotFoundException -> 214 | if not contextResponse.HasStarted then 215 | contextResponse.StatusCode <- 404 216 | return false, null 217 | } 218 | 219 | let encodeFile (enc:SupportedEncodings) (settings:CompressionSettings) (contextRequest:HttpRequest) (contextResponse:HttpResponse) (cancellationSrc:Threading.CancellationTokenSource) = 220 | task { 221 | let cancellationToken = cancellationSrc.Token 222 | if cancellationToken.IsCancellationRequested then () 223 | let! awaited = getFile settings contextRequest contextResponse cancellationSrc 224 | let shouldProcess, bytes = awaited 225 | if not shouldProcess then 226 | if isNull bytes then 227 | return () 228 | else 229 | #if NETSTANDARD21 230 | return! contextResponse.Body.WriteAsync(bytes.AsMemory(0, bytes.Length), cancellationToken) 231 | #else 232 | return! contextResponse.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken) 233 | #endif 234 | else 235 | 236 | use output = new MemoryStream() 237 | if not(contextResponse.Headers.ContainsKey "Vary") then 238 | contextResponse.Headers.Add("Vary", StringValues("Accept-Encoding")) 239 | use zipped = 240 | match enc with 241 | | Deflate -> 242 | contextResponse.Headers.Add("Content-Encoding", StringValues("deflate")) 243 | new DeflateStream(output, CompressionMode.Compress) :> Stream 244 | | GZip -> 245 | contextResponse.Headers.Add("Content-Encoding", StringValues("gzip")) 246 | new GZipStream(output, CompressionMode.Compress) :> Stream 247 | #if NETSTANDARD21 248 | let! t1 = zipped.WriteAsync(bytes.AsMemory(0, bytes.Length), cancellationToken) 249 | #else 250 | let! t1 = zipped.WriteAsync(bytes, 0, bytes.Length, cancellationToken) 251 | #endif 252 | t1 |> ignore 253 | zipped.Close() 254 | output.Close() 255 | let op = output.ToArray() 256 | 257 | let doStream = 258 | if cancellationToken.IsCancellationRequested then 259 | false 260 | else 261 | try 262 | let canStream = String.Equals(contextRequest.Protocol, "HTTP/1.1", StringComparison.Ordinal) 263 | if canStream && (int64 defaultBufferSize) < op.LongLength then 264 | if not(contextResponse.Headers.ContainsKey("Transfer-Encoding")) 265 | || contextResponse.Headers.["Transfer-Encoding"] <> StringValues("chunked") then 266 | contextResponse.Headers.["Transfer-Encoding"] <- "chunked" 267 | true 268 | else 269 | if (not contextResponse.ContentLength.HasValue) || (contextResponse.ContentLength.Value <> -1 && contextResponse.ContentLength.Value <> op.LongLength) then 270 | contextResponse.ContentLength <- Nullable op.LongLength 271 | false 272 | 273 | with | _ -> 274 | false // Content length info is not so important... 275 | 276 | if doStream then 277 | return! zipped.CopyToAsync(contextResponse.Body, defaultBufferSize, cancellationToken) 278 | else 279 | #if NETSTANDARD21 280 | return! contextResponse.Body.WriteAsync(op.AsMemory(0, op.Length), cancellationToken) 281 | #else 282 | return! contextResponse.Body.WriteAsync(op, 0, op.Length, cancellationToken) 283 | #endif 284 | } :> Task 285 | 286 | 287 | let compressableExtension (settings:CompressionSettings) (path:string) = 288 | match path with 289 | | null -> true 290 | #if NETSTANDARD21 291 | | x when x.Contains '.' -> 292 | #else 293 | | x when x.Contains "." -> 294 | #endif 295 | let typemap = settings.AllowedExtensionAndMimeTypes |> Map.ofSeq 296 | typemap.ContainsKey(x.Substring(x.LastIndexOf '.')) 297 | | _ -> false 298 | 299 | let encodeStream (enc:SupportedEncodings) (settings:CompressionSettings) (contextRequest:HttpRequest) (contextResponse:HttpResponse) (cancellationSrc:Threading.CancellationTokenSource) (next:Func) = 300 | let cancellationToken = cancellationSrc.Token 301 | let originalLengthNotEnough = contextResponse.Body.CanRead && contextResponse.Body.Length < settings.MinimumSizeToCompress 302 | let inline checkCompressability buffer = 303 | let inline captureResponse() = 304 | match buffer with 305 | | Some bufferStream -> 306 | contextResponse.Body <- bufferStream 307 | | None -> () 308 | let compressableExtension = compressableExtension settings (contextRequest.Path.ToString()) 309 | if compressableExtension then // non-stream, but Invoke can change "/" -> "index.html" 310 | captureResponse() 311 | true 312 | elif String.IsNullOrEmpty contextResponse.ContentType then 313 | if settings.AllowUnknonwnFiletypes then 314 | captureResponse() 315 | true 316 | else false 317 | else 318 | let contentType = 319 | // We are not interested of charset, etc: 320 | #if NETSTANDARD21 321 | match contextResponse.ContentType.Contains ';' with 322 | #else 323 | match contextResponse.ContentType.Contains ";" with 324 | #endif 325 | | false -> contextResponse.ContentType.ToLower() 326 | | true -> contextResponse.ContentType.Split(';').[0].ToLower() 327 | if settings.AllowedExtensionAndMimeTypes 328 | |> Seq.map snd |> Seq.append ["text/html"] 329 | |> Seq.contains(contentType) then 330 | captureResponse() 331 | true 332 | else 333 | false 334 | 335 | let isCompressable = 336 | (checkCompressability None) && not(settings.ExcludedPaths |> Seq.exists(fun p -> contextRequest.Path.ToString().Contains p)) 337 | && contextResponse.Body.CanWrite 338 | 339 | let inline continuation2 (pipedLengthNotEnough:bool) (copyBufferToBody:unit->Task) (copyBodyToCompressor:Stream->Task) (copyCompressedToBody:MemoryStream->Task) = 340 | task { 341 | 342 | let noCompression = 343 | (not contextResponse.Body.CanSeek) || (not contextResponse.Body.CanRead) 344 | || (originalLengthNotEnough && pipedLengthNotEnough) 345 | || (contextResponse.Headers.ContainsKey("Content-Encoding") && 346 | not(String.IsNullOrWhiteSpace(contextResponse.Headers.["Content-Encoding"]))) 347 | 348 | match noCompression with 349 | | true -> 350 | if contextResponse.Body.CanSeek then 351 | contextResponse.Body.Seek(0L, SeekOrigin.Begin) |> ignore 352 | do! copyBufferToBody() 353 | return () 354 | | false -> 355 | 356 | if not(contextResponse.Headers.ContainsKey "Vary") then 357 | contextResponse.Headers.Add("Vary", StringValues("Accept-Encoding")) 358 | 359 | use output = new MemoryStream() 360 | 361 | use zipped = 362 | match enc with 363 | | Deflate -> 364 | contextResponse.Headers.Add("Content-Encoding", StringValues("deflate")) 365 | new DeflateStream(output, CompressionMode.Compress) :> Stream 366 | | GZip -> 367 | contextResponse.Headers.Add("Content-Encoding", StringValues("gzip")) 368 | new GZipStream(output, CompressionMode.Compress) :> Stream 369 | //let! t1 = zipped.WriteAsync(bytes, 0, bytes.Length, cancellationToken) 370 | if contextResponse.Body.CanSeek then 371 | contextResponse.Body.Seek(0L, SeekOrigin.Begin) |> ignore 372 | 373 | do! copyBodyToCompressor(zipped) 374 | 375 | zipped.Close() 376 | output.Close() 377 | let op = output.ToArray() 378 | 379 | if not(cancellationToken.IsCancellationRequested) then 380 | try 381 | let canStream = String.Equals(contextRequest.Protocol, "HTTP/1.1", StringComparison.Ordinal) && not settings.StreamingDisabled 382 | if canStream && (int64 defaultBufferSize) < op.LongLength then 383 | if not(contextResponse.Headers.ContainsKey("Transfer-Encoding")) 384 | || contextResponse.Headers.["Transfer-Encoding"] <> StringValues("chunked") then 385 | contextResponse.Headers.["Transfer-Encoding"] <- StringValues("chunked") 386 | else 387 | if (not contextResponse.ContentLength.HasValue) || (contextResponse.ContentLength.Value <> -1 && contextResponse.ContentLength.Value <> op.LongLength) then 388 | contextResponse.ContentLength <- Nullable(op.LongLength) 389 | with | _ -> () // Content length info is not so important... 390 | 391 | use tmpOutput = new MemoryStream(op) 392 | if tmpOutput.CanSeek then 393 | tmpOutput.Seek(0L, SeekOrigin.Begin) |> ignore 394 | 395 | do! copyCompressedToBody tmpOutput 396 | return () 397 | } 398 | task { 399 | use streamWebOutput = contextResponse.Body 400 | use buffer = new MemoryStream() 401 | 402 | if isCompressable then 403 | contextResponse.Body <- buffer // stream 404 | else 405 | () 406 | 407 | do! next.Invoke() 408 | 409 | let pipedLengthNotEnough = 410 | contextResponse.Body.CanRead && 411 | (if contextResponse.Body.Length = 0 && contextResponse.Body = buffer && streamWebOutput.CanRead then streamWebOutput.Length else contextResponse.Body.Length) < settings.MinimumSizeToCompress 412 | 413 | let usecompress = isCompressable || checkCompressability (Some buffer) 414 | if usecompress && checkNoValidETag contextRequest contextResponse cancellationSrc contextResponse.Body then 415 | let inline copyBufferToBody() = 416 | task { 417 | do! contextResponse.Body.CopyToAsync(streamWebOutput, defaultBufferSize, cancellationToken) 418 | contextResponse.Body <- streamWebOutput 419 | } :> Task 420 | let inline copyBodyToCompressor (zipped:Stream) = contextResponse.Body.CopyToAsync(zipped, defaultBufferSize, cancellationToken) 421 | let inline copyCompressedToBody (zippedData:MemoryStream) = 422 | task { 423 | if zippedData.Length = 0 && streamWebOutput.CanRead then 424 | use output = new MemoryStream() 425 | use zipped = 426 | match enc with 427 | | Deflate -> 428 | new DeflateStream(output, CompressionMode.Compress) :> Stream 429 | | GZip -> 430 | new GZipStream(output, CompressionMode.Compress) :> Stream 431 | 432 | if streamWebOutput.CanSeek then 433 | streamWebOutput.Seek(0L, SeekOrigin.Begin) |> ignore 434 | 435 | do! streamWebOutput.CopyToAsync(zipped, defaultBufferSize, cancellationToken) 436 | zipped.Close() 437 | contextResponse.Body <- output 438 | else 439 | do! zippedData.CopyToAsync(streamWebOutput, defaultBufferSize, cancellationToken) 440 | contextResponse.Body <- streamWebOutput 441 | } :> Task 442 | return! continuation2 pipedLengthNotEnough copyBufferToBody copyBodyToCompressor copyCompressedToBody 443 | else 444 | return () 445 | } :> Task 446 | 447 | let inline internal compress (context:HttpContext) (settings:CompressionSettings) (mode:ResponseMode) = 448 | let cancellationSrc = System.Threading.CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted) 449 | let cancellationToken = cancellationSrc.Token 450 | 451 | let encodings = 452 | if cancellationToken.IsCancellationRequested then "" 453 | else 454 | match context.Request.Headers.TryGetValue "Accept-Encoding" with 455 | | true, x -> x.ToString() 456 | | false, _ -> "" 457 | let inline encodeOutput (enc:SupportedEncodings) = 458 | 459 | match settings.CacheExpireTime with 460 | | ValueSome d when not (context.Response.Headers.IsReadOnly) -> context.Response.Headers.["Expires"] <- StringValues(d.ToString()) 461 | | _ -> () 462 | 463 | match mode with 464 | | File -> encodeFile enc settings context.Request context.Response cancellationSrc 465 | | ContextResponseBody(next) -> 466 | 467 | if cancellationToken.IsCancellationRequested then 468 | task { 469 | do! next.Invoke() 470 | return () 471 | } 472 | else 473 | encodeStream enc settings context.Request context.Response cancellationSrc next 474 | 475 | let inline encodeTask() = 476 | let inline writeAsyncContext() = 477 | match mode with 478 | | File -> 479 | task { 480 | let! comp, r = getFile settings context.Request context.Response cancellationSrc 481 | if comp then 482 | #if NETSTANDARD21 483 | return! context.Response.Body.WriteAsync(r.AsMemory(0, r.Length), cancellationToken) 484 | #else 485 | return! context.Response.Body.WriteAsync(r, 0, r.Length, cancellationToken) 486 | #endif 487 | else return! Task.Delay 50 488 | } :> Task 489 | | ContextResponseBody(next) -> 490 | next.Invoke() 491 | if String.IsNullOrEmpty encodings then writeAsyncContext() 492 | elif encodings.Contains "deflate" && not(settings.DeflateDisabled) then encodeOutput Deflate 493 | elif encodings.Contains "gzip" then encodeOutput GZip 494 | else writeAsyncContext() 495 | 496 | encodeTask 497 | 498 | open OwinCompression 499 | 500 | [] 501 | type CompressionExtensions = 502 | 503 | [] 504 | static member UseCompressionModule(app:IApplicationBuilder, settings:CompressionSettings) = 505 | app.Use(fun context next -> 506 | (Internals.compress context settings (ResponseMode.ContextResponseBody next) )() 507 | ) 508 | 509 | [] 510 | static member UseCompressionModule(app:IApplicationBuilder) = 511 | CompressionExtensions.UseCompressionModule(app, DefaultCompressionSettings) 512 | 513 | /// You can set a path, which is the URL that will be captured. 514 | /// The subsequent url-path will be mapped to the server path. 515 | [] 516 | static member MapCompressionModule(app:IApplicationBuilder, path:PathString, settings:CompressionSettings) = 517 | app.Map(path, fun ap -> 518 | ap.Run(fun context -> 519 | (Internals.compress context settings ResponseMode.File)() 520 | )) 521 | 522 | [] 523 | static member UseCompressionModuleLogTime(app:IApplicationBuilder, settings:CompressionSettings) = 524 | app.Use(fun context next -> 525 | task { 526 | let sw = System.Diagnostics.Stopwatch.StartNew() 527 | let! r = (Internals.compress context settings (ResponseMode.ContextResponseBody next) )() 528 | sw.Stop() 529 | let measure = "Took " + sw.Elapsed.TotalMilliseconds.ToString() 530 | System.Diagnostics.Debug.WriteLine measure 531 | return r 532 | } :> Task 533 | ) 534 | 535 | /// You can set a path, which is the URL that will be captured. 536 | /// The subsequent url-path will be mapped to the server path. 537 | /// Uses OwinCompression.DefaultCompressionSettings 538 | [] 539 | static member MapCompressionModule(app:IApplicationBuilder, path:PathString) = 540 | CompressionExtensions.MapCompressionModule(app, path, DefaultCompressionSettings) 541 | -------------------------------------------------------------------------------- /src/Owin.Compression.Standard/Owin.Compression.Standard.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | true 6 | NETSTANDARD 7 | ..\..\bin 8 | Owin.Compression 9 | Owin.Compression 10 | 8.0.0.0 11 | https://github.com/Thorium/Owin.Compression 12 | owin, compression, gzip, aspnetcore, webserver, deflate, etag, kestrel, speedup, request, compress, middleware, pipeline 13 | false 14 | git 15 | Thorium 16 | fixed-right 17 | false 18 | https://github.com/Thorium/Owin.Compression/raw/master/docs/files/img/logo.png 19 | default 20 | NETSTANDARD21 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | AssemblyInfo.fs 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Owin.Compression/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | do () 11 | 12 | module internal AssemblyVersionInformation = 13 | let [] AssemblyTitle = "Owin.Compression" 14 | let [] AssemblyProduct = "Owin.Compression" 15 | let [] AssemblyDescription = "Compression (Deflate / GZip) module for Microsoft OWIN Selfhost filesystem pipeline." 16 | let [] AssemblyVersion = "1.0.46" 17 | let [] AssemblyFileVersion = "1.0.46" 18 | -------------------------------------------------------------------------------- /src/Owin.Compression/CompressionModule.fs: -------------------------------------------------------------------------------- 1 | namespace Owin 2 | 3 | open System 4 | open System.IO 5 | open System.IO.Compression 6 | open Owin 7 | open Microsoft.Owin 8 | open System.Threading.Tasks 9 | open System.Runtime.CompilerServices 10 | open System.Collections.Generic 11 | 12 | /// Supported compression methods 13 | [] 14 | type SupportedEncodings = 15 | | Deflate 16 | | GZip 17 | 18 | /// Do you fetch files, or do you encode context.Response.Body? 19 | type ResponseMode = 20 | | File 21 | | ContextResponseBody of Next: Func 22 | 23 | /// Settings for compression. 24 | type CompressionSettings = { 25 | ServerPath: string; 26 | AllowUnknonwnFiletypes: bool; 27 | AllowRootDirectories: bool; 28 | CacheExpireTime: DateTimeOffset voption; 29 | AllowedExtensionAndMimeTypes: IEnumerable; 30 | MinimumSizeToCompress: int64; 31 | DeflateDisabled: bool; 32 | StreamingDisabled: bool; 33 | ExcludedPaths: IEnumerable; 34 | } 35 | 36 | module OwinCompression = 37 | #if INTERACTIVE 38 | let basePath = __SOURCE_DIRECTORY__; 39 | #else 40 | let basePath = System.Reflection.Assembly.GetExecutingAssembly().Location |> Path.GetDirectoryName 41 | #endif 42 | /// Default settings for compression. 43 | let DefaultCompressionSettings = { 44 | ServerPath = basePath; 45 | AllowUnknonwnFiletypes = false; 46 | AllowRootDirectories = false; 47 | CacheExpireTime = ValueNone 48 | MinimumSizeToCompress = 1000L 49 | DeflateDisabled = false 50 | StreamingDisabled = false 51 | ExcludedPaths = [| "/signalr/" |] 52 | AllowedExtensionAndMimeTypes = 53 | [| 54 | ".js" , "application/javascript"; 55 | ".css" , "text/css"; 56 | ".yml" , "application/x-yaml"; 57 | ".json" , "application/json"; 58 | ".svg" , "image/svg+xml"; 59 | ".txt" , "text/plain"; 60 | ".html" , "application/json"; // we don't want to follow hyperlinks, so not "text/html" 61 | ".map" , "application/octet-stream"; 62 | ".ttf" , "application/x-font-ttf"; 63 | ".otf" , "application/x-font-opentype"; 64 | ".ico" , "image/x-icon"; 65 | ".map" , "application/json"; 66 | ".xml" , "application/xml"; 67 | ".xsl" , "text/xml"; 68 | ".xhtml", "application/xhtml+xml"; 69 | ".rss" , "application/rss+xml"; 70 | ".eot" , "font/eot"; 71 | ".aspx" , "text/html"; 72 | |] 73 | } 74 | 75 | /// Default settings with custom path. No cache time. 76 | let DefaultCompressionSettingsWithPath path = 77 | {DefaultCompressionSettings with 78 | ServerPath = path; CacheExpireTime = ValueSome (DateTimeOffset.Now.AddDays 7.) } 79 | 80 | /// Default settings with custom path and cache-time. C#-helper method. 81 | let DefaultCompressionSettingsWithPathAndCache(path,cachetime) = 82 | {DefaultCompressionSettings with ServerPath = path; CacheExpireTime = ValueSome (cachetime) } 83 | 84 | let private defaultBufferSize = 81920 85 | 86 | module Internals = 87 | 88 | let getHash (item:Stream) = 89 | if item.CanRead then 90 | let hasPos = 91 | if item.CanSeek && item.Position > 0L then 92 | let tmp = item.Position 93 | item.Position <- 0L 94 | ValueSome tmp 95 | else ValueNone 96 | use md5 = System.Security.Cryptography.MD5.Create() 97 | let res = BitConverter.ToString(md5.ComputeHash item).Replace("-","") 98 | match hasPos with 99 | | ValueSome x when item.CanSeek -> item.Position <- x 100 | | _ -> () 101 | ValueSome res 102 | else ValueNone 103 | 104 | let inline create304Response (contextResponse:IOwinResponse) = 105 | if contextResponse.StatusCode <> 304 then 106 | contextResponse.StatusCode <- 304 107 | contextResponse.Body.Close() 108 | contextResponse.Body <- Stream.Null 109 | if contextResponse.ContentLength.HasValue then 110 | contextResponse.ContentLength <- Nullable() 111 | false 112 | 113 | let checkNoValidETag (contextRequest:IOwinRequest) (contextResponse:IOwinResponse) (cancellationSrc:Threading.CancellationTokenSource) (itemToCheck:Stream) = 114 | 115 | if contextRequest.Headers.ContainsKey("If-None-Match") && (not(isNull contextRequest.Headers.["If-None-Match"])) && 116 | (not(contextRequest.Headers.ContainsKey("Pragma")) || contextRequest.Headers.["Pragma"] <> "no-cache") then 117 | let noneMatch = contextRequest.Headers.["If-None-Match"] 118 | if noneMatch = contextResponse.ETag then 119 | if not (isNull cancellationSrc) then cancellationSrc.Cancel() 120 | create304Response contextResponse 121 | else 122 | 123 | match getHash itemToCheck with 124 | | ValueSome etag -> 125 | if noneMatch = etag then 126 | if not (isNull cancellationSrc) then cancellationSrc.Cancel() 127 | create304Response contextResponse 128 | else 129 | if String.IsNullOrEmpty contextResponse.ETag && 130 | not contextResponse.Headers.IsReadOnly then 131 | contextResponse.ETag <- etag 132 | true 133 | | ValueNone -> true 134 | else 135 | if String.IsNullOrEmpty contextResponse.ETag then 136 | match getHash itemToCheck with 137 | | ValueSome etag when not contextResponse.Headers.IsReadOnly && itemToCheck.Length > 0L -> 138 | contextResponse.ETag <- etag 139 | | _ -> () 140 | true 141 | 142 | let internal getFile (settings:CompressionSettings) (contextRequest:IOwinRequest) (contextResponse:IOwinResponse) (cancellationSrc:Threading.CancellationTokenSource) = 143 | let unpacked :string = 144 | let p = contextRequest.Path.ToString() 145 | if not(settings.AllowRootDirectories) && p.Contains ".." then failwith "Invalid path" 146 | if File.Exists p then failwith "Invalid resource" 147 | let p2 = 148 | match p.StartsWith "/" with 149 | | true -> p.Substring 1 150 | | false -> p 151 | Path.Combine ([| settings.ServerPath; p2|]) 152 | 153 | let extension = if not (unpacked.Contains ".") then "" else unpacked.Substring(unpacked.LastIndexOf '.') 154 | let typemap = settings.AllowedExtensionAndMimeTypes |> Map.ofSeq 155 | 156 | match typemap.TryGetValue extension with 157 | | true, extval -> contextResponse.ContentType <- extval 158 | | false, _ when settings.AllowUnknonwnFiletypes -> () 159 | | _ -> 160 | contextResponse.StatusCode <- 415 161 | raise (ArgumentException("Invalid resource type", contextRequest.Path.ToString())) 162 | 163 | task { 164 | try 165 | use strm = File.OpenText unpacked 166 | let! txt = strm.ReadToEndAsync() 167 | let bytes = txt |> System.Text.Encoding.UTF8.GetBytes 168 | match FileInfo(unpacked).Length < settings.MinimumSizeToCompress with 169 | | true -> 170 | return false, bytes 171 | | false -> 172 | let lastmodified = File.GetLastWriteTimeUtc(unpacked).ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", System.Globalization.CultureInfo.InvariantCulture) 173 | contextResponse.Headers.Add("Last-Modified", [|lastmodified|]) 174 | return 175 | if checkNoValidETag contextRequest contextResponse cancellationSrc strm.BaseStream then 176 | true, bytes 177 | else 178 | false, null 179 | with 180 | | :? FileNotFoundException -> 181 | contextResponse.StatusCode <- 404 182 | return false, null 183 | } 184 | 185 | let encodeFile (enc:SupportedEncodings) (settings:CompressionSettings) (contextRequest:IOwinRequest) (contextResponse:IOwinResponse) (cancellationSrc:Threading.CancellationTokenSource)= 186 | task { 187 | let cancellationToken = cancellationSrc.Token 188 | if cancellationToken.IsCancellationRequested then () 189 | let! awaited = getFile settings contextRequest contextResponse cancellationSrc 190 | let shouldProcess, bytes = awaited 191 | if not shouldProcess then 192 | if isNull bytes then 193 | return () 194 | else 195 | return! contextResponse.WriteAsync(bytes, cancellationToken) 196 | else 197 | 198 | use output = new MemoryStream() 199 | if not(contextResponse.Headers.ContainsKey "Vary") then 200 | contextResponse.Headers.Add("Vary", [|"Accept-Encoding"|]) 201 | use zipped = 202 | match enc with 203 | | Deflate -> 204 | contextResponse.Headers.Add("Content-Encoding", [|"deflate"|]) 205 | new DeflateStream(output, CompressionMode.Compress) :> Stream 206 | | GZip -> 207 | contextResponse.Headers.Add("Content-Encoding", [|"gzip"|]) 208 | new GZipStream(output, CompressionMode.Compress) :> Stream 209 | let! t1 = zipped.WriteAsync(bytes, 0, bytes.Length, cancellationToken) 210 | t1 |> ignore 211 | zipped.Close() 212 | output.Close() 213 | 214 | let op = output.ToArray() 215 | 216 | let doStream = 217 | if cancellationToken.IsCancellationRequested then 218 | false 219 | else 220 | try 221 | let canStream = String.Equals(contextRequest.Protocol, "HTTP/1.1", StringComparison.Ordinal) 222 | if canStream && (int64 defaultBufferSize) < op.LongLength then 223 | if not(contextResponse.Headers.ContainsKey("Transfer-Encoding")) 224 | || contextResponse.Headers.["Transfer-Encoding"] <> "chunked" then 225 | contextResponse.Headers.["Transfer-Encoding"] <- "chunked" 226 | true 227 | else 228 | if (not contextResponse.ContentLength.HasValue) || (contextResponse.ContentLength.Value <> -1 && contextResponse.ContentLength.Value <> op.LongLength) then 229 | contextResponse.ContentLength <- Nullable op.LongLength 230 | false 231 | 232 | with | _ -> 233 | false // Content length info is not so important... 234 | 235 | if doStream then 236 | return! zipped.CopyToAsync(contextResponse.Body, defaultBufferSize, cancellationToken) 237 | else 238 | return! contextResponse.WriteAsync(op, cancellationToken) 239 | } :> Task 240 | 241 | let compressableExtension (settings:CompressionSettings) (path:string) = 242 | match path with 243 | | null -> true 244 | | x when x.Contains(".") -> 245 | let typemap = settings.AllowedExtensionAndMimeTypes |> Map.ofSeq 246 | typemap.ContainsKey(x.Substring(x.LastIndexOf '.')) 247 | | _ -> false 248 | 249 | let encodeStream (enc:SupportedEncodings) (settings:CompressionSettings) (contextRequest:IOwinRequest) (contextResponse:IOwinResponse) (cancellationSrc:Threading.CancellationTokenSource) (next:Func) = 250 | 251 | let cancellationToken = cancellationSrc.Token 252 | let originalLengthNotEnough = contextResponse.Body.CanRead && contextResponse.Body.Length < settings.MinimumSizeToCompress 253 | let inline checkCompressability buffer = 254 | let inline captureResponse() = 255 | match buffer with 256 | | Some bufferStream -> 257 | contextResponse.Body <- bufferStream 258 | | None -> () 259 | let compressableExtension = compressableExtension settings (contextRequest.Path.ToString()) 260 | if compressableExtension then // non-stream, but Invoke can change "/" -> "index.html" 261 | captureResponse() 262 | true 263 | elif String.IsNullOrEmpty contextResponse.ContentType then 264 | if settings.AllowUnknonwnFiletypes then 265 | captureResponse() 266 | true 267 | else false 268 | else 269 | let contentType = 270 | // We are not interested of charset, etc: 271 | match contextResponse.ContentType.Contains(";") with 272 | | false -> contextResponse.ContentType.ToLower() 273 | | true -> contextResponse.ContentType.Split(';').[0].ToLower() 274 | if settings.AllowedExtensionAndMimeTypes 275 | |> Seq.map snd |> Seq.append ["text/html"] 276 | |> Seq.contains(contentType) then 277 | captureResponse() 278 | true 279 | else 280 | false 281 | 282 | let isCompressable = 283 | (checkCompressability None) && not(settings.ExcludedPaths |> Seq.exists(fun p -> contextRequest.Path.ToString().Contains p)) 284 | && contextResponse.Body.CanWrite 285 | 286 | let inline continuation2 (pipedLengthNotEnough:bool) (copyBufferToBody:unit->Task) (copyBodyToCompressor:Stream->Task) (copyCompressedToBody:MemoryStream->Task) = 287 | task { 288 | 289 | let noCompression = 290 | (not contextResponse.Body.CanSeek) || (not contextResponse.Body.CanRead) 291 | || (originalLengthNotEnough && pipedLengthNotEnough) 292 | || (contextResponse.Headers.ContainsKey("Content-Encoding") && 293 | not(String.IsNullOrWhiteSpace(contextResponse.Headers.["Content-Encoding"]))) 294 | 295 | match noCompression with 296 | | true -> 297 | if contextResponse.Body.CanSeek then 298 | contextResponse.Body.Seek(0L, SeekOrigin.Begin) |> ignore 299 | do! copyBufferToBody() 300 | return () 301 | | false -> 302 | 303 | if not(contextResponse.Headers.ContainsKey "Vary") then 304 | contextResponse.Headers.Add("Vary", [|"Accept-Encoding"|]) 305 | 306 | use output = new MemoryStream() 307 | 308 | use zipped = 309 | match enc with 310 | | Deflate -> 311 | contextResponse.Headers.Add("Content-Encoding", [|"deflate"|]) 312 | new DeflateStream(output, CompressionMode.Compress) :> Stream 313 | | GZip -> 314 | contextResponse.Headers.Add("Content-Encoding", [|"gzip"|]) 315 | new GZipStream(output, CompressionMode.Compress) :> Stream 316 | //let! t1 = zipped.WriteAsync(bytes, 0, bytes.Length, cancellationToken) 317 | if contextResponse.Body.CanSeek then 318 | contextResponse.Body.Seek(0L, SeekOrigin.Begin) |> ignore 319 | 320 | do! copyBodyToCompressor(zipped) 321 | 322 | zipped.Close() 323 | output.Close() 324 | let op = output.ToArray() 325 | 326 | if not(cancellationToken.IsCancellationRequested) then 327 | try 328 | let canStream = String.Equals(contextRequest.Protocol, "HTTP/1.1", StringComparison.Ordinal) && not settings.StreamingDisabled 329 | if canStream && (int64 defaultBufferSize) < op.LongLength then 330 | if not(contextResponse.Headers.ContainsKey("Transfer-Encoding")) 331 | || contextResponse.Headers.["Transfer-Encoding"] <> "chunked" then 332 | contextResponse.Headers.["Transfer-Encoding"] <- "chunked" 333 | else 334 | if (not contextResponse.ContentLength.HasValue) || (contextResponse.ContentLength.Value <> -1 && contextResponse.ContentLength.Value <> op.LongLength) then 335 | contextResponse.ContentLength <- Nullable(op.LongLength) 336 | with | _ -> () // Content length info is not so important... 337 | 338 | use tmpOutput = new MemoryStream(op) 339 | if tmpOutput.CanSeek then 340 | tmpOutput.Seek(0L, SeekOrigin.Begin) |> ignore 341 | 342 | do! copyCompressedToBody tmpOutput 343 | return () 344 | } 345 | 346 | task { 347 | use streamWebOutput = contextResponse.Body 348 | use buffer = new MemoryStream() 349 | 350 | if isCompressable then 351 | contextResponse.Body <- buffer // stream 352 | else 353 | () 354 | 355 | do! next.Invoke() 356 | 357 | let pipedLengthNotEnough = 358 | contextResponse.Body.CanRead && 359 | (if contextResponse.Body.Length = 0 && contextResponse.Body = buffer && streamWebOutput.CanRead then streamWebOutput.Length else contextResponse.Body.Length) < settings.MinimumSizeToCompress 360 | 361 | let usecompress = isCompressable || checkCompressability (Some buffer) 362 | if usecompress && checkNoValidETag contextRequest contextResponse cancellationSrc (if contextResponse.Body.Length > 0L then contextResponse.Body else streamWebOutput) then 363 | 364 | let inline copyBufferToBody() = 365 | task { 366 | do! contextResponse.Body.CopyToAsync(streamWebOutput, defaultBufferSize, cancellationToken) 367 | contextResponse.Body <- streamWebOutput 368 | } :> Task 369 | let inline copyBodyToCompressor (zipped:Stream) = contextResponse.Body.CopyToAsync(zipped, defaultBufferSize, cancellationToken) 370 | let inline copyCompressedToBody (zippedData:MemoryStream) = 371 | task { 372 | if zippedData.Length = 0 && streamWebOutput.CanRead then 373 | 374 | use output = new MemoryStream() 375 | use zipped = 376 | match enc with 377 | | Deflate -> 378 | new DeflateStream(output, CompressionMode.Compress) :> Stream 379 | | GZip -> 380 | new GZipStream(output, CompressionMode.Compress) :> Stream 381 | 382 | if streamWebOutput.CanSeek then 383 | streamWebOutput.Seek(0L, SeekOrigin.Begin) |> ignore 384 | 385 | do! streamWebOutput.CopyToAsync(zipped, defaultBufferSize, cancellationToken) 386 | zipped.Close() 387 | contextResponse.Body <- output 388 | else 389 | do! zippedData.CopyToAsync(streamWebOutput, defaultBufferSize, cancellationToken) 390 | contextResponse.Body <- streamWebOutput 391 | } :> Task 392 | return! continuation2 pipedLengthNotEnough copyBufferToBody copyBodyToCompressor copyCompressedToBody 393 | 394 | else 395 | return () 396 | } :> Task 397 | 398 | 399 | let inline internal compress (context:IOwinContext) (settings:CompressionSettings) (mode:ResponseMode) = 400 | let cancellationSrc = System.Threading.CancellationTokenSource.CreateLinkedTokenSource(context.Request.CallCancelled) 401 | let cancellationToken = cancellationSrc.Token 402 | 403 | let encodings = 404 | if cancellationToken.IsCancellationRequested then "" 405 | else context.Request.Headers.["Accept-Encoding"] 406 | 407 | let inline encodeOutput (enc:SupportedEncodings) = 408 | 409 | match settings.CacheExpireTime with 410 | | ValueSome d when not (context.Response.Headers.IsReadOnly) -> context.Response.Expires <- Nullable(d) 411 | | _ -> () 412 | 413 | match mode with 414 | | File -> encodeFile enc settings context.Request context.Response cancellationSrc 415 | | ContextResponseBody(next) -> 416 | 417 | 418 | if cancellationToken.IsCancellationRequested then 419 | task { 420 | do! next.Invoke() 421 | return () 422 | } 423 | else 424 | 425 | encodeStream enc settings context.Request context.Response cancellationSrc next 426 | 427 | let inline encodeTask() = 428 | let inline writeAsyncContext() = 429 | match mode with 430 | | File -> 431 | task { 432 | let! comp, r = getFile settings context.Request context.Response cancellationSrc 433 | if comp then 434 | return! context.Response.WriteAsync(r, cancellationToken) 435 | else return! Task.Delay 50 436 | } :> Task 437 | | ContextResponseBody(next) -> 438 | next.Invoke() 439 | if String.IsNullOrEmpty encodings then writeAsyncContext() 440 | elif encodings.Contains "deflate" && not(settings.DeflateDisabled) then encodeOutput Deflate 441 | elif encodings.Contains "gzip" then encodeOutput GZip 442 | else writeAsyncContext() 443 | 444 | encodeTask 445 | 446 | open OwinCompression 447 | 448 | [] 449 | type CompressionExtensions = 450 | 451 | [] 452 | static member UseCompressionModule(app:IAppBuilder, settings:CompressionSettings) = 453 | app.Use(fun context next -> 454 | (Internals.compress context settings (ResponseMode.ContextResponseBody next) )() 455 | ) 456 | 457 | [] 458 | static member UseCompressionModule(app:IAppBuilder) = 459 | CompressionExtensions.UseCompressionModule(app, DefaultCompressionSettings) 460 | 461 | /// You can set a path, which is the URL that will be captured. 462 | /// The subsequent url-path will be mapped to the server path. 463 | [] 464 | static member MapCompressionModule(app:IAppBuilder, path:string, settings:CompressionSettings) = 465 | app.Map(path, fun ap -> 466 | ap.Run(fun context -> 467 | (Internals.compress context settings ResponseMode.File)() 468 | )) 469 | 470 | /// You can set a path, which is the URL that will be captured. 471 | /// The subsequent url-path will be mapped to the server path. 472 | /// Uses OwinCompression.DefaultCompressionSettings 473 | [] 474 | static member MapCompressionModule(app:IAppBuilder, path:string) = 475 | CompressionExtensions.MapCompressionModule(app, path, DefaultCompressionSettings) 476 | -------------------------------------------------------------------------------- /src/Owin.Compression/Owin.Compression.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Library 6 | Owin.Compression 7 | Owin.Compression 8 | ..\..\bin 9 | net48 10 | v4.8 11 | 8.0.0.0 12 | Owin.Compression 13 | https://github.com/Thorium/Owin.Compression 14 | false 15 | 3239;3511;$(WarningsAsErrors) 16 | owin, compression, gzip, aspnetcore, webserver, deflate, etag, kestrel, speedup, request, compress, middleware, pipeline 17 | false 18 | git 19 | Thorium 20 | fixed-right 21 | false 22 | https://github.com/Thorium/Owin.Compression/raw/master/docs/files/img/logo.png 23 | default 24 | README.md 25 | 26 | 27 | 4 28 | ..\..\bin\net472\Owin.Compression.xml 29 | 30 | 31 | true 32 | true 33 | 34 | 35 | true 36 | true 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/Owin.Compression/Script.fsx: -------------------------------------------------------------------------------- 1 | // Learn more about F# at http://fsharp.org. See the 'F# Tutorial' project 2 | // for more guidance on F# programming. 3 | #I "../../bin" 4 | #I @"./../../packages/Owin/lib/net40" 5 | #I @"./../../packages/Microsoft.Owin/lib/net451" 6 | #I @"./../../packages/Microsoft.Owin.Hosting/lib/net451" 7 | #I @"./../../bin/Owin.Compression" 8 | 9 | #r "Owin.dll" 10 | #r "Microsoft.Owin.dll" 11 | #r "Microsoft.Owin.Hosting.dll" 12 | #r "System.Configuration.dll" 13 | #r "Owin.Compression.dll" 14 | 15 | open System 16 | 17 | #load "CompressionModule.fs" 18 | open Owin 19 | 20 | printfn "%s" (OwinCompression.DefaultCompressionSettings.ToString()) 21 | -------------------------------------------------------------------------------- /src/Owin.Compression/nuget.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Owin.Compression/paket.references: -------------------------------------------------------------------------------- 1 | Owin 2 | FSharp.Core 3 | Microsoft.Owin 4 | -------------------------------------------------------------------------------- /src/Owin.Compression/paket.template: -------------------------------------------------------------------------------- 1 | type project 2 | title 3 | Owin.Compression 4 | owners 5 | Tuomas Hietanen 6 | authors 7 | Tuomas Hietanen 8 | projectUrl 9 | http://github.com/Thorium/Owin.Compression 10 | iconUrl 11 | https://raw.githubusercontent.com/Thorium/Owin.Compression/master/docs/files/img/logo.png 12 | licenseUrl 13 | http://github.com/Thorium/Owin.Compression/blob/master/LICENSE.txt 14 | requireLicenseAcceptance 15 | false 16 | copyright 17 | Copyright 2015 18 | tags 19 | OWIN SelfHost eTag AspNetCore GZip Deflate compress pack self host file system pipeline Microsoft 20 | ASPNET ASP.NET Core Web API Kestrel caching 21 | summary 22 | Compression (Deflate / GZip) and eTag caching module for Microsoft OWIN Selfhost filesystem pipeline. 23 | It supports also AspNetCore, for example running WebAPI on .NET6.0+ and Kestrel. 24 | description 25 | Compression (Deflate / GZip) module for Microsoft OWIN self-host and AspNetCore/Kestrel web servers. Built-in eTag caching. With this module you can compress, deflate / gzip large files (like concatenated *.js or *.css files) to the reduce amount of web traffic. 26 | dependencies 27 | framework: net48 28 | Owin >= 1.0.0 29 | FSharp.Core >= LOCKEDVERSION 30 | Microsoft.Owin ~> LOCKEDVERSION 31 | framework: netstandard20 32 | FSharp.Core >= LOCKEDVERSION 33 | Microsoft.AspNetCore.Http.Abstractions >= LOCKEDVERSION-standard 34 | Microsoft.AspNetCore.Http.Features >= LOCKEDVERSION-standard 35 | Microsoft.Extensions.Primitives >= LOCKEDVERSION-standard 36 | framework: netstandard21 37 | FSharp.Core >= LOCKEDVERSION 38 | Microsoft.AspNetCore.Http.Abstractions >= LOCKEDVERSION-standard 39 | Microsoft.AspNetCore.Http.Features >= LOCKEDVERSION-standard 40 | Microsoft.Extensions.Primitives >= LOCKEDVERSION-standard 41 | files 42 | ../../bin/**/Owin.Compression.dll 43 | ../../bin/**/Owin.Compression.xml 44 | ../../bin/**/Owin.Compression.deps.json 45 | -------------------------------------------------------------------------------- /tests/Aspnet.Core.WebAPI.Test/Aspnet.Core.WebAPI.Test.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/Aspnet.Core.WebAPI.Test/Aspnet.Core.WebAPI.Test.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33110.190 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Aspnet.Core.WebAPI.Test", "Aspnet.Core.WebAPI.Test.fsproj", "{323B8523-18A2-4F59-AB4E-CFC200E3D68D}" 7 | EndProject 8 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Owin.Compression.Standard", "..\..\src\Owin.Compression.Standard\Owin.Compression.Standard.fsproj", "{08152038-56C7-4F36-9436-DCA0F64C1E5F}" 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 | {323B8523-18A2-4F59-AB4E-CFC200E3D68D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {323B8523-18A2-4F59-AB4E-CFC200E3D68D}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {323B8523-18A2-4F59-AB4E-CFC200E3D68D}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {323B8523-18A2-4F59-AB4E-CFC200E3D68D}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {08152038-56C7-4F36-9436-DCA0F64C1E5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {08152038-56C7-4F36-9436-DCA0F64C1E5F}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {08152038-56C7-4F36-9436-DCA0F64C1E5F}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {08152038-56C7-4F36-9436-DCA0F64C1E5F}.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 = {C2869B05-305D-4B72-9D2D-E19D17F8DC60} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /tests/Aspnet.Core.WebAPI.Test/Controllers/WeatherForecastController.fs: -------------------------------------------------------------------------------- 1 | namespace Aspnet.Core.WebAPI.Test.Controllers 2 | 3 | open System 4 | open System.Collections.Generic 5 | open System.Linq 6 | open System.Threading.Tasks 7 | open Microsoft.AspNetCore.Mvc 8 | open Microsoft.Extensions.Logging 9 | open Aspnet.Core.WebAPI.Test 10 | 11 | [] 12 | [] 13 | type WeatherForecastController (logger : ILogger) = 14 | inherit ControllerBase() 15 | 16 | let summaries = 17 | [| 18 | "Freezing" 19 | "Bracing" 20 | "Chilly" 21 | "Cool" 22 | "Mild" 23 | "Warm" 24 | "Balmy" 25 | "Hot" 26 | "Sweltering" 27 | "Scorching" 28 | |] 29 | 30 | [] 31 | member _.Get() = 32 | let rng = System.Random() 33 | [| 34 | for index in 0..1000 -> 35 | { Date = DateTime.Now.AddDays(float index) 36 | TemperatureC = rng.Next(-20,55) 37 | Summary = summaries.[rng.Next(summaries.Length)] } 38 | |] 39 | -------------------------------------------------------------------------------- /tests/Aspnet.Core.WebAPI.Test/Program.fs: -------------------------------------------------------------------------------- 1 | namespace Aspnet.Core.WebAPI.Test 2 | #nowarn "20" 3 | open System 4 | open System.Collections.Generic 5 | open System.IO 6 | open System.Linq 7 | open System.Threading.Tasks 8 | open Microsoft.AspNetCore 9 | open Microsoft.AspNetCore.Builder 10 | open Microsoft.AspNetCore.Hosting 11 | open Microsoft.AspNetCore.HttpsPolicy 12 | open Microsoft.Extensions.Configuration 13 | open Microsoft.Extensions.DependencyInjection 14 | open Microsoft.Extensions.Hosting 15 | open Microsoft.Extensions.Logging 16 | open Owin 17 | 18 | module Program = 19 | let exitCode = 0 20 | 21 | [] 22 | let main args = 23 | 24 | let builder = WebApplication.CreateBuilder(args) 25 | 26 | builder.Services.AddControllers() 27 | 28 | let app = builder.Build() 29 | 30 | let compressionSetting = 31 | {OwinCompression.DefaultCompressionSettings with 32 | CacheExpireTime = ValueSome (DateTimeOffset.Now.AddDays 7.) 33 | AllowUnknonwnFiletypes = true 34 | StreamingDisabled = true 35 | MinimumSizeToCompress = 0 36 | } 37 | 38 | (app :> IApplicationBuilder).UseCompressionModule(compressionSetting) |> ignore 39 | 40 | app.MapControllers() 41 | 42 | app.Run() 43 | 44 | exitCode 45 | -------------------------------------------------------------------------------- /tests/Aspnet.Core.WebAPI.Test/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:33058", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "Aspnet.Core.WebAPI.Test": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "weatherforecast", 17 | "applicationUrl": "http://localhost:5161", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "weatherforecast", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Aspnet.Core.WebAPI.Test/WeatherForecast.fs: -------------------------------------------------------------------------------- 1 | namespace Aspnet.Core.WebAPI.Test 2 | 3 | open System 4 | 5 | type WeatherForecast = 6 | { Date: DateTime 7 | TemperatureC: int 8 | Summary: string } 9 | 10 | member this.TemperatureF = 11 | 32.0 + (float this.TemperatureC / 0.5556) 12 | -------------------------------------------------------------------------------- /tests/Aspnet.Core.WebAPI.Test/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/Aspnet.Core.WebAPI.Test/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /tests/Aspnet.Core.WebAPI.Test/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core -------------------------------------------------------------------------------- /tests/Owin.Compression.Tests/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/Owin.Compression.Tests/Owin.Compression.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Library 6 | 7 | Owin.Compression.Tests 8 | Owin.Compression.Tests 9 | net481 10 | v4.8.1 11 | true 12 | 8.0.0.0 13 | Owin.Compression.Tests 14 | false 15 | false 16 | 17 | 18 | bin\ 19 | 4 20 | 21 | 22 | true 23 | true 24 | 25 | 26 | true 27 | true 28 | 29 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Owin.Compression 48 | {1869df4f-917c-4051-9d88-3701561b13fc} 49 | True 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/Owin.Compression.Tests/Tests.fs: -------------------------------------------------------------------------------- 1 | namespace Owin.Compression.Test 2 | 3 | open Owin 4 | open System 5 | open FsUnit.Xunit 6 | open System.Collections.Generic 7 | open System.Threading.Tasks 8 | open Xunit 9 | 10 | open Owin 11 | open System 12 | open Microsoft 13 | 14 | module MockOwin = 15 | let generateResponse (contentBody:string option) = 16 | let mutable etag = "" 17 | let mutable body = 18 | match contentBody with 19 | | Some content -> new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes content) :> System.IO.Stream 20 | | None -> new System.IO.MemoryStream() :> System.IO.Stream 21 | let mutable status = 200 22 | let setBody v = 23 | body <- v 24 | 25 | let headers = Owin.HeaderDictionary(Dictionary()) 26 | { new Microsoft.Owin.IOwinResponse with 27 | member this.Body 28 | with get () = body 29 | and set v = setBody v 30 | member this.ContentLength with get () = Nullable(body.Length) and set v = () 31 | member this.ContentType with get () = "html" and set v = () 32 | member this.Context = raise (System.NotImplementedException()) 33 | member this.Cookies = raise (System.NotImplementedException()) 34 | member this.ETag with get () = etag and set v = etag <- v 35 | member this.Environment = raise (System.NotImplementedException()) 36 | member this.Expires with get () = Nullable(DateTime.Today.AddMonths 1) and set v = () 37 | member this.Get(key) = raise (System.NotImplementedException()) 38 | member this.Headers = headers 39 | member this.OnSendingHeaders(callback, state) = raise (System.NotImplementedException()) 40 | member this.Protocol with get () = "http" and set v = () 41 | member this.ReasonPhrase with get () = "" and set v = () 42 | member this.Redirect(location) = raise (System.NotImplementedException()) 43 | member this.Set(key, value) = raise (System.NotImplementedException()) 44 | member this.StatusCode with get () = status and set v = status <- v 45 | member this.Write(text: string): unit = () 46 | member this.Write(data: byte array): unit = () 47 | member this.Write(data: byte array, offset: int, count: int): unit = () 48 | member this.WriteAsync(text: string): Task = task { return () } :> Task 49 | member this.WriteAsync(text: string, token: Threading.CancellationToken): Task = task { return () } :> Task 50 | member this.WriteAsync(data: byte array): Task = body.WriteAsync(data, 0, data.Length) 51 | member this.WriteAsync(data: byte array, token: Threading.CancellationToken): Task = body.WriteAsync(data, 0, data.Length) 52 | member this.WriteAsync(data: byte array, offset: int, count: int, token: Threading.CancellationToken): Task = body.WriteAsync(data, 0, data.Length) 53 | } 54 | let generateRequest() = 55 | let headers = Owin.HeaderDictionary(Dictionary()) 56 | headers.Add("Accept-Encoding", [|"gzip"|]) 57 | let mutable path = "/index.html" 58 | { new Microsoft.Owin.IOwinRequest with 59 | member this.Accept 60 | with get () = raise (System.NotImplementedException()) 61 | and set v = raise (System.NotImplementedException()) 62 | member this.Body 63 | with get () = raise (System.NotImplementedException()) 64 | and set v = raise (System.NotImplementedException()) 65 | member this.CacheControl 66 | with get () = raise (System.NotImplementedException()) 67 | and set v = raise (System.NotImplementedException()) 68 | member this.CallCancelled 69 | with get () = raise (System.NotImplementedException()) 70 | and set v = raise (System.NotImplementedException()) 71 | member this.ContentType 72 | with get () = raise (System.NotImplementedException()) 73 | and set v = raise (System.NotImplementedException()) 74 | member this.Context = raise (System.NotImplementedException()) 75 | member this.Cookies = raise (System.NotImplementedException()) 76 | member this.Environment = raise (System.NotImplementedException()) 77 | member this.Get(key) = raise (System.NotImplementedException()) 78 | member this.Headers = headers 79 | member this.Host 80 | with get () = raise (System.NotImplementedException()) 81 | and set v = raise (System.NotImplementedException()) 82 | member this.IsSecure = raise (System.NotImplementedException()) 83 | member this.LocalIpAddress 84 | with get () = raise (System.NotImplementedException()) 85 | and set v = raise (System.NotImplementedException()) 86 | member this.LocalPort 87 | with get () = raise (System.NotImplementedException()) 88 | and set v = raise (System.NotImplementedException()) 89 | member this.MediaType 90 | with get () = raise (System.NotImplementedException()) 91 | and set v = raise (System.NotImplementedException()) 92 | member this.Method 93 | with get () = raise (System.NotImplementedException()) 94 | and set v = raise (System.NotImplementedException()) 95 | member this.Path 96 | with get () = Owin.PathString path 97 | and set v = path <- v.Value 98 | member this.PathBase 99 | with get () = raise (System.NotImplementedException()) 100 | and set v = raise (System.NotImplementedException()) 101 | member this.Protocol 102 | with get () = "http" 103 | and set v = () 104 | member this.Query = raise (System.NotImplementedException()) 105 | member this.QueryString 106 | with get () = raise (System.NotImplementedException()) 107 | and set v = raise (System.NotImplementedException()) 108 | member this.ReadFormAsync() = raise (System.NotImplementedException()) 109 | member this.RemoteIpAddress 110 | with get () = raise (System.NotImplementedException()) 111 | and set v = raise (System.NotImplementedException()) 112 | member this.RemotePort 113 | with get () = raise (System.NotImplementedException()) 114 | and set v = raise (System.NotImplementedException()) 115 | member this.Scheme 116 | with get () = raise (System.NotImplementedException()) 117 | and set v = raise (System.NotImplementedException()) 118 | member this.Set(key, value) = raise (System.NotImplementedException()) 119 | member this.Uri = raise (System.NotImplementedException()) 120 | member this.User 121 | with get () = raise (System.NotImplementedException()) 122 | and set v = raise (System.NotImplementedException()) 123 | } 124 | 125 | module WebStartFileServer = 126 | type MyWebStartup() = 127 | member __.Configuration(app:Owin.IAppBuilder) = 128 | let compressionSetting = 129 | {OwinCompression.DefaultCompressionSettings with 130 | CacheExpireTime = ValueSome (DateTimeOffset.Now.AddDays 7.) 131 | } 132 | app.MapCompressionModule("/zipped", compressionSetting) |> ignore 133 | () 134 | 135 | [)>] 136 | do() 137 | 138 | module WebStart = 139 | type MyWebStartup() = 140 | member __.Configuration(app:Owin.IAppBuilder) = 141 | let compressionSetting = 142 | {OwinCompression.DefaultCompressionSettings with 143 | CacheExpireTime = ValueSome (DateTimeOffset.Now.AddDays 7.) 144 | } 145 | 146 | app.UseCompressionModule(compressionSetting) |> ignore 147 | () 148 | 149 | [)>] 150 | do() 151 | 152 | type ``Server fixture`` () = 153 | [] 154 | member test.``safe default settings`` () = 155 | let settings = OwinCompression.DefaultCompressionSettings 156 | settings.AllowUnknonwnFiletypes |> should equal false 157 | settings.AllowRootDirectories |> should equal false 158 | 159 | [] 160 | member test. ``Server can be started when MapCompressionModule is used`` () = 161 | // May need admin rights 162 | use server = Microsoft.Owin.Hosting.WebApp.Start "http://*:8080" 163 | System.Threading.Thread.Sleep 3000 164 | // You can uncomment this, debug the test and go to localhost to observe how system works: 165 | // System.Console.ReadLine() |> ignore 166 | Assert.NotNull server 167 | 168 | [] 169 | member test. ``Server can be started when UseCompressionModule is used`` () = 170 | use server = Microsoft.Owin.Hosting.WebApp.Start "http://*:8080" 171 | System.Threading.Thread.Sleep 3000 172 | Assert.NotNull server 173 | 174 | type ``Compress internals fixture`` () = 175 | 176 | [] 177 | member test. ``GetHash should be consistent`` () = 178 | use ms = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes "hello") 179 | let h = OwinCompression.Internals.getHash ms 180 | Assert.Equal(ValueSome "5D41402ABC4B2A76B9719D911017C592", h) 181 | 182 | [] 183 | member test. ``Html should be combressable extension`` () = 184 | let isC = OwinCompression.Internals.compressableExtension OwinCompression.DefaultCompressionSettings "/file.html" 185 | Assert.True isC 186 | 187 | [] 188 | member test. ``Set status cached`` () = 189 | let mockResponse = MockOwin.generateResponse(Some "hello") 190 | let dne = OwinCompression.Internals.create304Response mockResponse 191 | Assert.Equal(304, mockResponse.StatusCode) 192 | 193 | [] 194 | member test. ``ETag mismatch test`` () = 195 | let mockResponse = MockOwin.generateResponse(Some "hello") 196 | let mockRequest = Owin.OwinRequest() 197 | mockRequest.Headers.Add("If-None-Match", [|"abc"|]) 198 | let noEtag = OwinCompression.Internals.checkNoValidETag mockRequest mockResponse (new Threading.CancellationTokenSource()) mockResponse.Body 199 | Assert.NotEqual(304, mockResponse.StatusCode) 200 | Assert.Equal("5D41402ABC4B2A76B9719D911017C592", mockResponse.ETag) 201 | Assert.True noEtag 202 | 203 | [] 204 | member test. ``ETag match test`` () = 205 | let mockResponse = MockOwin.generateResponse(Some "hello") 206 | let mockRequest = Owin.OwinRequest() 207 | mockRequest.Headers.Add("If-None-Match", [|"5D41402ABC4B2A76B9719D911017C592"|]) 208 | let noEtag = OwinCompression.Internals.checkNoValidETag mockRequest mockResponse (new Threading.CancellationTokenSource()) mockResponse.Body 209 | Assert.Equal(304, mockResponse.StatusCode) 210 | Assert.False noEtag 211 | 212 | [] 213 | member test. ``Compress stream test skips small`` () = 214 | task { 215 | let mockResponse = MockOwin.generateResponse(Some "hello") 216 | let mockRequest = Owin.OwinRequest() 217 | let taskReturn = Func(fun _ -> task { return () } :> Task) 218 | let! res = OwinCompression.Internals.encodeStream SupportedEncodings.Deflate OwinCompression.DefaultCompressionSettings mockRequest mockResponse (new Threading.CancellationTokenSource()) taskReturn 219 | Assert.NotNull mockResponse.Body 220 | Assert.Equal(200,mockResponse.StatusCode) 221 | let content = (mockResponse.Body :?> System.IO.MemoryStream).ToArray() |> System.Text.Encoding.UTF8.GetString 222 | Assert.Equal("hello",content) 223 | Assert.False (mockResponse.Headers.ContainsKey "ETag") 224 | return () 225 | } :> Task 226 | 227 | [] 228 | member test. ``Compress stream no-pipeline test`` () = 229 | task { 230 | let longstring = [|1 .. 100_000|] |> Array.map(fun _ -> "x") |> String.concat "abcab460iw3[pn ZWV$dZZo1093ba0v|!!Äcx0c23" 231 | let mockResponse = MockOwin.generateResponse (Some longstring) 232 | 233 | let mockRequest = MockOwin.generateRequest() 234 | let taskReturn = Func(fun _ -> task { return () } :> Task) 235 | let! isOk = OwinCompression.Internals.encodeStream SupportedEncodings.Deflate OwinCompression.DefaultCompressionSettings mockRequest mockResponse (new Threading.CancellationTokenSource()) taskReturn 236 | Assert.NotNull mockResponse.Body 237 | Assert.Equal(200,mockResponse.StatusCode) 238 | let content = (mockResponse.Body :?> System.IO.MemoryStream).ToArray() |> System.Text.Encoding.UTF8.GetString 239 | Assert.True(content.Length < longstring.Length, "wasn't compressed") 240 | Assert.True(content.Length > 0, "Result shouldn't be empty") 241 | Assert.Equal("3FFF606E12076433E80412E5048FF643", mockResponse.ETag) 242 | return () 243 | } :> Task 244 | 245 | [] 246 | member test. ``Compress stream pipeline test`` () = 247 | task { 248 | let longstring = [|1 .. 100_000|] |> Array.map(fun _ -> "x") |> String.concat "abcab460iw3[pn ZWV$dZZo1093ba0v|!!Äcx0c23" 249 | let mockResponse = MockOwin.generateResponse None 250 | let mockRequest = MockOwin.generateRequest() 251 | let mutable pipelineProcessing = 0 252 | let taskReturn = Func(fun _ -> 253 | task { 254 | mockResponse.Body <- new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes longstring) :> System.IO.Stream 255 | pipelineProcessing <- 1 256 | return () } :> Task) 257 | let! isOk = OwinCompression.Internals.encodeStream SupportedEncodings.Deflate OwinCompression.DefaultCompressionSettings mockRequest mockResponse (new Threading.CancellationTokenSource()) taskReturn 258 | Assert.NotNull mockResponse.Body 259 | Assert.Equal(200,mockResponse.StatusCode) 260 | Assert.Equal(1,pipelineProcessing) 261 | let content = (mockResponse.Body :?> System.IO.MemoryStream).ToArray() |> System.Text.Encoding.UTF8.GetString 262 | Assert.True(content.Length < longstring.Length, "wasn't compressed") 263 | Assert.True(content.Length > 0, "Result shouldn't be empty") 264 | Assert.Equal("3FFF606E12076433E80412E5048FF643", mockResponse.ETag) 265 | return () 266 | } :> Task 267 | 268 | [] 269 | member test. ``Compress file test`` () = 270 | task { 271 | let mockResponse = MockOwin.generateResponse None 272 | let mockRequest = MockOwin.generateRequest() 273 | mockRequest.Path <- Owin.PathString "/Owin.Compression.DLL" 274 | let settings = { OwinCompression.DefaultCompressionSettings with AllowUnknonwnFiletypes = true } 275 | let! isOk = OwinCompression.Internals.encodeFile SupportedEncodings.Deflate settings mockRequest mockResponse (new Threading.CancellationTokenSource()) 276 | Assert.NotNull mockResponse.Body 277 | Assert.Equal(200,mockResponse.StatusCode) 278 | Assert.NotNull mockResponse.ETag 279 | let content = (mockResponse.Body :?> System.IO.MemoryStream).ToArray() 280 | Assert.True(content.Length > 0) 281 | 282 | return () 283 | } :> Task 284 | 285 | (* 286 | open BenchmarkDotNet.Attributes 287 | open BenchmarkDotNet.Running 288 | open BenchmarkDotNet.Jobs 289 | 290 | [] [] 291 | type Benchmarks() = 292 | 293 | let longstring = [|1 .. 100_000|] |> Array.map(fun _ -> "x") |> String.concat "abcab460iw3[pn ZWV$dZZo1093ba0v|!!Äcx0c23" |> System.Text.Encoding.UTF8.GetBytes 294 | 295 | [] 296 | member this.CompressPipeline () = 297 | task { 298 | let mockResponse = MockOwin.generateResponse None 299 | let mockRequest = MockOwin.generateRequest() 300 | let taskReturn = Func(fun _ -> 301 | task { 302 | mockResponse.Body <- new System.IO.MemoryStream(longstring) :> System.IO.Stream 303 | return () } :> Task) 304 | let! isOk = OwinCompression.Internals.encodeStream SupportedEncodings.Deflate OwinCompression.DefaultCompressionSettings mockRequest mockResponse (new Threading.CancellationTokenSource()) taskReturn 305 | return 1 306 | } 307 | 308 | module BenchmarkTest = 309 | 310 | BenchmarkRunner.Run( 311 | BenchmarkDotNet.Configs.ManualConfig 312 | .Create(BenchmarkDotNet.Configs.DefaultConfig.Instance) 313 | .WithOptions(BenchmarkDotNet.Configs.ConfigOptions.DisableOptimizationsValidator)) 314 | |> ignore 315 | 316 | // To run: Change outpuy type from Library to Exe, then: 317 | // dotnet build --configuration Release 318 | // dotnet run --configuration Release 319 | 320 | // .NET 4.8.1 321 | // | Method | Mean | Error | StdDev | Allocated | 322 | // |----------------- |---------:|---------:|---------:|----------:| 323 | // | CompressPipeline | 17.23 ms | 0.228 ms | 0.213 ms | 77.81 KB | 324 | 325 | // .NET 8.0 326 | // | Method | Mean | Error | StdDev | Gen0 | Allocated | 327 | // |----------------- |---------:|----------:|----------:|-------:|----------:| 328 | // | CompressPipeline | 6.738 ms | 0.0527 ms | 0.0493 ms | 7.8125 | 98.15 KB | 329 | 330 | *) 331 | -------------------------------------------------------------------------------- /tests/Owin.Compression.Tests/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | Owin 3 | Microsoft.Owin 4 | Microsoft.Owin.Hosting 5 | Microsoft.Owin.Host.HttpListener 6 | 7 | group Test 8 | FsUnit.xUnit 9 | xunit.abstractions 10 | xunit.core 11 | xunit.runner.visualstudio 12 | BenchmarkDotNet 13 | --------------------------------------------------------------------------------
PM> Install-Package Owin.Compression