├── tests ├── Aspnet.Core.WebAPI.Test │ ├── paket.references │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── WeatherForecast.fs │ ├── Aspnet.Core.WebAPI.Test.fsproj │ ├── Properties │ │ └── launchSettings.json │ ├── Controllers │ │ └── WeatherForecastController.fs │ ├── Program.fs │ └── Aspnet.Core.WebAPI.Test.sln └── Owin.Compression.Tests │ ├── paket.references │ ├── App.config │ ├── Owin.Compression.Tests.fsproj │ └── Tests.fs ├── screen.png ├── src ├── Owin.Compression │ ├── paket.references │ ├── nuget.Config │ ├── Script.fsx │ ├── AssemblyInfo.fs │ ├── paket.template │ ├── Owin.Compression.fsproj │ └── CompressionModule.fs └── Owin.Compression.Standard │ ├── AssemblyInfo.fs │ ├── Owin.Compression.Standard.fsproj │ └── CompressionModule.fs ├── docs ├── files │ └── img │ │ ├── logo.png │ │ └── logo-template.pdn └── content │ ├── tutorial.fsx │ ├── index.fsx │ └── content │ └── fsdocs-custom.css ├── .paket ├── paket.bootstrapper.exe ├── paket.targets └── Paket.Restore.targets ├── .travis.yml ├── appveyor.yml ├── nuget.Config ├── lib └── README.md ├── .gitattributes ├── .editorconfig ├── .config └── dotnet-tools.json ├── README.md ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.yaml │ └── bug_report.yaml └── pull_request_template.md ├── paket.dependencies ├── LICENSE.txt ├── Owin.Compression.sln ├── .gitignore ├── netfx.props └── RELEASE_NOTES.md /tests/Aspnet.Core.WebAPI.Test/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thorium/Owin.Compression/HEAD/screen.png -------------------------------------------------------------------------------- /src/Owin.Compression/paket.references: -------------------------------------------------------------------------------- 1 | Owin 2 | FSharp.Core 3 | Microsoft.Owin 4 | -------------------------------------------------------------------------------- /docs/files/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thorium/Owin.Compression/HEAD/docs/files/img/logo.png -------------------------------------------------------------------------------- /.paket/paket.bootstrapper.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thorium/Owin.Compression/HEAD/.paket/paket.bootstrapper.exe -------------------------------------------------------------------------------- /docs/files/img/logo-template.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thorium/Owin.Compression/HEAD/docs/files/img/logo-template.pdn -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /nuget.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Owin.Compression/nuget.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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.50" 17 | let [] AssemblyFileVersion = "1.0.50" 18 | -------------------------------------------------------------------------------- /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.50" 17 | let [] AssemblyFileVersion = "1.0.50" 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Owin.Compression 3 | 4 | Compression (Deflate / GZip / Brotli) 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, gzip, or brotli) 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, brotli, and streaming if you prefer. 11 | 12 | **Note:** Brotli compression is available only when targeting .NET Standard 2.1 or higher (e.g., .NET 8.0, .NET 6.0), using ASP.NET Core's built-in BrotliStream support. 13 | 14 | This project works on C# and F# and should work on all .NET platforms, also on Windows, and even Mono as well. 15 | 16 | 17 | Here is a demo in action from Fiddler net traffic monitor: 18 | 19 | ![compressed](screen.png) 20 | 21 | Read the [Getting started tutorial](https://thorium.github.io/Owin.Compression/index.html#Getting-started) to learn more. 22 | 23 | Documentation: https://thorium.github.io/Owin.Compression 24 | 25 | 26 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | nuget System.Text.Encodings.Web 17 | 18 | // nuget Microsoft.AspNet.WebApi.OwinSelfHost 19 | // nuget Microsoft.Owin.StaticFiles 20 | 21 | group Build 22 | source https://nuget.org/api/v2 23 | 24 | nuget Microsoft.SourceLink.GitHub 25 | nuget FAKE.Core.Target 26 | nuget FAKE.Core.ReleaseNotes 27 | nuget FAKE.DotNet.Cli 28 | nuget FAKE.DotNet.AssemblyInfoFile 29 | nuget FAKE.Tools.Git 30 | nuget FAKE.IO.FileSystem 31 | nuget FAKE.DotNet.Testing.XUnit 32 | nuget FSharp.Formatting 33 | 34 | github fsharp/FAKE modules/Octokit/Octokit.fsx 35 | 36 | group Test 37 | source https://nuget.org/api/v2 38 | 39 | nuget BenchmarkDotNet 40 | nuget xunit.core 41 | nuget xunit.abstractions 42 | nuget xunit.runner.visualstudio >= 2.0 version_in_path: true 43 | nuget xunit.runner.console 44 | nuget FsUnit.xUnit 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | System.Text.Encodings.Web >= LOCKEDVERSION-standard 37 | framework: netstandard21 38 | FSharp.Core >= LOCKEDVERSION 39 | Microsoft.AspNetCore.Http.Abstractions >= LOCKEDVERSION-standard 40 | Microsoft.AspNetCore.Http.Features >= LOCKEDVERSION-standard 41 | Microsoft.Extensions.Primitives >= LOCKEDVERSION-standard 42 | System.Text.Encodings.Web >= LOCKEDVERSION-standard 43 | files 44 | ../../bin/**/Owin.Compression.dll 45 | ../../bin/**/Owin.Compression.xml 46 | ../../bin/**/Owin.Compression.deps.json 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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.Standard/Owin.Compression.Standard.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1;net8.0 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, brotli, 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 | embedded 26 | true 27 | true 28 | true 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | AssemblyInfo.fs 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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/ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 1.0.50 - October 27 2025 2 | * Fix to not expect all files are UTF8 encoded 3 | 4 | ### 1.0.49 - October 27 2025 5 | * Brotli support added in NetStandard2.1 and NET6-... via .NET built-in functions. 6 | 7 | ### 1.0.48 - September 08 2025 8 | * Minor performance optimisations 9 | * Dependency updates 10 | 11 | ### 1.0.47 - December 13 2024 12 | * Nuget package infromation update 13 | 14 | ### 1.0.46 - June 21 2024 15 | * Performance optimizations when on .NET Standard 2.1 (NET 6.0 / NET 8.0) 16 | 17 | ### 1.0.45 - April 1 2024 18 | * NuGet Net Framework 4.7.2 dependency corrected to version 4.8 19 | 20 | ### 1.0.44 - April 1 2024 21 | * More tolerant pipeline config. 22 | * Minor performance improvements. 23 | * Package frameworks to .NET 4.8, .NET Standard 2.0, and 2.1 24 | 25 | ### 1.0.41 - March 22 2024 26 | * ASP.NET Core side reference updates 27 | 28 | ### 1.0.40 - March 22 2024 29 | * Reference component updates, PR #13 30 | * Package frameworks to .NET 4.7.2, .NET Standard 2.0, and 2.1 31 | 32 | ### 1.0.38 - November 1 2023 33 | * Less exception catching on runtime 34 | 35 | ### 1.0.37 - November 1 2023 36 | * Minor performance improvements 37 | * FSharp.Core update 38 | 39 | ### 1.0.34 - October 7 2023 40 | * Minor performance improvements 41 | 42 | ### 1.0.33 - March 7 2023 43 | * Dependency update: Microsoft.Extensions.Primitives 44 | 45 | ### 1.0.32 - November 23 2022 46 | * Improvements on error handling 47 | * Improvements on picking which files to compress 48 | 49 | ### 1.0.30 - November 18 2022 50 | * StreamingDisabled setting for disabling HTTP1.1 streaming responses 51 | * Streaming added on static files 52 | * AspNET Core WebAPI .NET Standard fixes (and test project added) 53 | 54 | ### 1.0.29 - August 11 2022 55 | * Package dependency update 56 | 57 | ### 1.0.28 - August 10 2022 58 | * VS2022 update 59 | * Fixed task compilation 60 | 61 | ### 1.0.26 - June 17 2022 62 | * Reference component update 63 | * Better performance via F# 6.0 tasks 64 | 65 | ### 1.0.24 - March 22 2022 66 | * Fix for avoiding double compression 67 | 68 | ### 1.0.23 - July 13 2021 69 | * Reference component update 70 | 71 | ### 1.0.22 - February 01 2021 72 | * Reference component update 73 | 74 | ### 1.0.21 - July 11 2018 75 | * Reference component update 76 | 77 | ### 1.0.20 - February 20 2018 78 | * Check for cancellation token before reading headers 79 | 80 | ### 1.0.19 - February 20 2018 81 | * References updated 82 | 83 | ### 1.0.18 - November 08 2017 84 | * Fix for stream ETAGs #8 85 | 86 | ### 1.0.17 - October 02 2017 87 | * Fix for Owin dependency #7 88 | * Initial conversion for .NET Standard 2.0 using Microsoft.AspNetCore.Http, not tested yet. 89 | 90 | ### 1.0.16 - June 09 2017 91 | * References updated 92 | 93 | ### 1.0.15 - April 26 2017 94 | * References updated 95 | 96 | ### 1.0.14 - March 20 2017 97 | * Minor default config update 98 | 99 | ### 1.0.13 - March 08 2017 100 | * No functionality changes. 101 | * Dependency updated. 102 | 103 | ### 1.0.12 - September 10 2016 104 | * Respect Pragma no-cache 105 | 106 | ### 1.0.11 - September 06 2016 107 | * Added compression based on Mime type 108 | * eTag cache: cancel work if can send 304 109 | 110 | ### 1.0.10 - June 30 2016 111 | * Added setting option DeflateDisabled 112 | 113 | ### 1.0.9 - June 29 2016 114 | * Added reference to FSharp.Core 115 | 116 | ### 1.0.8 - April 15 2016 117 | * Added Vary-header part 3. 118 | 119 | ### 1.0.7 - April 15 2016 120 | * Added Vary-header part 2. 121 | 122 | ### 1.0.6 - April 15 2016 123 | * Added Vary-header. 124 | 125 | ### 1.0.5 - April 06 2016 126 | * Don't compress SignalR requests 127 | 128 | ### 1.0.4 - April 06 2016 129 | * Better handling of canceled request. 130 | 131 | ### 1.0.3 - April 04 2016 132 | * Better handling of canceled request. 133 | 134 | ### 1.0.2 - March 11 2016 135 | * Added support for static files with app.UseCompressionModule() 136 | 137 | ### 1.0.1 - December 11 2015 138 | * Documentation and C# interface improved 139 | 140 | ### 1.0 - December 11 2015 141 | * Initial release 142 | -------------------------------------------------------------------------------- /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 / Brotli) 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 | Brotli is supported only in .NET Standard 2.1 or higher (e.g., .NET 8.0, .NET 6.0), using ASP.NET Core's built-in BrotliStream support. 33 | This also supports streaming responses. The config allows you to disable deflate and streaming if you prefer. 34 | 35 | 36 | eTag-caching 37 | ---------- 38 | 39 | 1. When the server reads the content before compression, it calculates a hash-code over it. 40 | 2. The hash-code is sent as ETag response header to the client with the response 41 | 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. 42 | 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". 43 | 44 | 45 | Example #1 46 | ---------- 47 | 48 | This example demonstrates using MapCompressionModule-function defined in this sample library. 49 | 50 | ```csharp 51 | using System; 52 | using Owin; 53 | [assembly: Microsoft.Owin.OwinStartup(typeof(MyServer.MyWebStartup))] 54 | namespace MyServer 55 | { 56 | class MyWebStartup 57 | { 58 | public void Configuration(Owin.IAppBuilder app) 59 | { 60 | // This will compress the whole request, if you want to use e.g. Microsoft.Owin.StaticFiles server: 61 | // app.UseCompressionModule() 62 | 63 | var settings = OwinCompression.DefaultCompressionSettingsWithPath("c:\\temp\\"); //" server path 64 | //or var settings = new CompressionSettings( ... ) 65 | app.MapCompressionModule("/zipped", settings); 66 | } 67 | } 68 | 69 | class Program 70 | { 71 | static void Main(string[] args) 72 | { 73 | Microsoft.Owin.Hosting.WebApp.Start("http://*:8080"); //" run on localhost. 74 | Console.WriteLine("Server started... Press enter to exit."); 75 | Console.ReadLine(); 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | And now your files are smaller than with e.g. just Microsoft.Owin.StaticFiles -library server: 82 | 83 | compressed 84 | 85 | Even though the browser sees everything as plain text, the traffic is actually transferred in compressed format. 86 | You can monitor the traffic with e.g. Fiddler. 87 | 88 | Example #2 89 | ---------- 90 | 91 | Running on OWIN Self-Host (Microsoft.Owin.Hosting) with static files server (Microsoft.Owin.StaticFiles) 92 | and compressing only the ".json"-responses (and files) on-the-fly, with only gzip and not deflate: 93 | 94 | ```csharp 95 | using System; 96 | using Owin; 97 | [assembly: Microsoft.Owin.OwinStartup(typeof(MyServer.MyWebStartup))] 98 | namespace MyServer 99 | { 100 | class MyWebStartup 101 | { 102 | public void Configuration(Owin.IAppBuilder app) 103 | { 104 | var settings = new CompressionSettings( 105 | serverPath: "", 106 | allowUnknonwnFiletypes: false, 107 | allowRootDirectories: false, 108 | cacheExpireTime: Microsoft.FSharp.Core.FSharpOption.None, 109 | allowedExtensionAndMimeTypes: 110 | new[] { Tuple.Create(".json", "application/json") }, 111 | minimumSizeToCompress: 1000, 112 | streamingDisabled: false, 113 | deflateDisabled: true 114 | ); 115 | app.UseCompressionModule(settings); 116 | } 117 | } 118 | 119 | class Program 120 | { 121 | static void Main(string[] args) 122 | { 123 | Microsoft.Owin.Hosting.WebApp.Start("http://*:8080"); 124 | Console.WriteLine("Server started... Press enter to exit."); 125 | Console.ReadLine(); 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | Example #3 132 | ---------- 133 | 134 | Running on OWIN Self-Host (Microsoft.Owin.Hosting) with static files server (Microsoft.Owin.StaticFiles) 135 | and compressing all the responses (and files) on-the-fly. This example is in F-Sharp (and can be run with F#-interactive): 136 | 137 | *) 138 | 139 | 140 | #r "Owin.dll" 141 | #r "Microsoft.Owin.dll" 142 | #r "Microsoft.Owin.FileSystems.dll" 143 | #r "Microsoft.Owin.Hosting.dll" 144 | #r "Microsoft.Owin.StaticFiles.dll" 145 | #r "System.Configuration.dll" 146 | #r "Owin.Compression.dll" 147 | 148 | open Owin 149 | open System 150 | 151 | module Examples = 152 | 153 | type MyStartup() = 154 | member __.Configuration(app:Owin.IAppBuilder) = 155 | let app1 = app.UseCompressionModule() 156 | app1.UseFileServer "/." |> ignore 157 | () 158 | 159 | let server = Microsoft.Owin.Hosting.WebApp.Start "http://*:6000" 160 | Console.WriteLine "Press Enter to stop & quit." 161 | Console.ReadLine() |> ignore 162 | server.Dispose() 163 | 164 | (** 165 | 166 | Example #4 167 | ---------- 168 | 169 | Running on ASP.NET Core web API on .NET 6.0. You can use C# but this example is in F# 170 | just because of the shorter syntax. The full project is available in tests-folder of this project: 171 | 172 | *) 173 | 174 | open System 175 | open Microsoft.AspNetCore.Builder 176 | open Microsoft.Extensions.DependencyInjection 177 | open Microsoft.Extensions.Hosting 178 | open Owin 179 | 180 | module Program = 181 | 182 | [] 183 | let main args = 184 | 185 | let builder = WebApplication.CreateBuilder args 186 | builder.Services.AddControllers() |> ignore 187 | let app = builder.Build() 188 | 189 | let compressionSetting = 190 | {OwinCompression.DefaultCompressionSettings with 191 | CacheExpireTime = Some (DateTimeOffset.Now.AddDays 7.) 192 | AllowUnknonwnFiletypes = true 193 | StreamingDisabled = true 194 | } 195 | (app :> IApplicationBuilder).UseCompressionModule(compressionSetting) |> ignore 196 | app.MapControllers() |> ignore 197 | app.Run() 198 | 0 199 | (** 200 | 201 | https://github.com/Thorium/Owin.Compression/tree/master/tests/Aspnet.Core.WebAPI.Test 202 | 203 | Example #5 204 | ---------- 205 | 206 | More complete examples can be found here. 207 | 208 | 209 | Samples & documentation 210 | ----------------------- 211 | 212 | The library comes with comprehensible documentation. 213 | It can include tutorials automatically generated from `*.fsx` files in [the content folder][content]. 214 | The API reference is automatically generated from Markdown comments in the library implementation. 215 | 216 | * [Tutorial](tutorial.html) contains a further explanation of this sample library. 217 | 218 | * [API Reference](reference/index.html) contains automatically generated documentation for all types, modules 219 | and functions in the library. This includes additional brief samples on using most of the 220 | functions. 221 | 222 | Contributing and copyright 223 | -------------------------- 224 | 225 | The project is hosted on [GitHub][gh] where you can [report issues][issues], fork 226 | the project and submit pull requests. If you're adding a new public API, please also 227 | consider adding [samples][content] that can be turned into documentation. You might 228 | also want to read the [library design notes][readme] to understand how it works. 229 | 230 | The library is available under a Public Domain license, which allows modification and 231 | redistribution for both commercial and non-commercial purposes. For more information see the 232 | [License file][license] in the GitHub repository. 233 | 234 | [content]: https://github.com/fsprojects/Owin.Compression/tree/master/docs/content 235 | [gh]: https://github.com/fsprojects/Owin.Compression 236 | [issues]: https://github.com/fsprojects/Owin.Compression/issues 237 | [readme]: https://github.com/fsprojects/Owin.Compression/blob/master/README.md 238 | [license]: https://github.com/fsprojects/Owin.Compression/blob/master/LICENSE.txt 239 | *) 240 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/json"; 62 | ".ttf" , "application/x-font-ttf"; 63 | ".otf" , "application/x-font-opentype"; 64 | ".ico" , "image/x-icon"; 65 | ".xml" , "application/xml"; 66 | ".xsl" , "text/xml"; 67 | ".xhtml", "application/xhtml+xml"; 68 | ".rss" , "application/rss+xml"; 69 | ".eot" , "font/eot"; 70 | ".aspx" , "text/html"; 71 | |] 72 | } 73 | 74 | /// Default settings with custom path. No cache time. 75 | let DefaultCompressionSettingsWithPath path = 76 | {DefaultCompressionSettings with 77 | ServerPath = path; CacheExpireTime = ValueSome (DateTimeOffset.Now.AddDays 7.) } 78 | 79 | /// Default settings with custom path and cache-time. C#-helper method. 80 | let DefaultCompressionSettingsWithPathAndCache(path,cachetime) = 81 | {DefaultCompressionSettings with ServerPath = path; CacheExpireTime = ValueSome (cachetime) } 82 | 83 | let private defaultBufferSize = 81920 84 | 85 | /// Cached extension to MIME type mapping for performance 86 | let private getExtensionMap (settings:CompressionSettings) = 87 | // Cache the map computation to avoid repeated Map.ofSeq operations 88 | settings.AllowedExtensionAndMimeTypes |> Map.ofSeq 89 | 90 | module Internals = 91 | 92 | // Use thread-safe MD5 computation for better performance 93 | let private computeHash (item:Stream) = 94 | use md5 = System.Security.Cryptography.MD5.Create() 95 | md5.ComputeHash item 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 | let hashBytes = computeHash item 106 | let res = BitConverter.ToString(hashBytes).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:IOwinResponse) = 114 | if contextResponse.StatusCode <> 304 then 115 | contextResponse.StatusCode <- 304 116 | contextResponse.Body.Close() 117 | contextResponse.Body <- Stream.Null 118 | if contextResponse.ContentLength.HasValue then 119 | contextResponse.ContentLength <- Nullable() 120 | false 121 | 122 | let checkNoValidETag (contextRequest:IOwinRequest) (contextResponse:IOwinResponse) (cancellationSrc:Threading.CancellationTokenSource) (itemToCheck:Stream) = 123 | 124 | if contextRequest.Headers.ContainsKey("If-None-Match") && (not(isNull contextRequest.Headers.["If-None-Match"])) && 125 | (not(contextRequest.Headers.ContainsKey("Pragma")) || contextRequest.Headers.["Pragma"] <> "no-cache") then 126 | let noneMatch = contextRequest.Headers.["If-None-Match"] 127 | if noneMatch = contextResponse.ETag then 128 | if not (isNull cancellationSrc) then cancellationSrc.Cancel() 129 | create304Response contextResponse 130 | else 131 | 132 | match getHash itemToCheck with 133 | | ValueSome etag -> 134 | if noneMatch = etag then 135 | if not (isNull cancellationSrc) then cancellationSrc.Cancel() 136 | create304Response contextResponse 137 | else 138 | if String.IsNullOrEmpty contextResponse.ETag && 139 | not contextResponse.Headers.IsReadOnly then 140 | contextResponse.ETag <- etag 141 | true 142 | | ValueNone -> true 143 | else 144 | if String.IsNullOrEmpty contextResponse.ETag then 145 | match getHash itemToCheck with 146 | | ValueSome etag when not contextResponse.Headers.IsReadOnly && itemToCheck.Length > 0L -> 147 | contextResponse.ETag <- etag 148 | | _ -> () 149 | true 150 | 151 | let internal getFile (settings:CompressionSettings) (contextRequest:IOwinRequest) (contextResponse:IOwinResponse) (cancellationSrc:Threading.CancellationTokenSource) = 152 | let unpacked :string = 153 | let p = contextRequest.Path.ToString() 154 | if not(settings.AllowRootDirectories) && p.Contains ".." then failwith $"Invalid path: {p}" 155 | if File.Exists p then failwith $"Invalid resource: {p}" 156 | let p2 = 157 | match p.StartsWith "/" with 158 | | true -> p.Substring 1 159 | | false -> p 160 | let unpackedPath = Path.Combine ([| settings.ServerPath; p2|]) 161 | let fullPath = Path.GetFullPath unpackedPath |> Path.GetDirectoryName 162 | if not(settings.AllowRootDirectories) then 163 | if not(fullPath.StartsWith(settings.ServerPath, StringComparison.OrdinalIgnoreCase)) then failwith $"Tried to access invalid path: {p}" 164 | unpackedPath 165 | 166 | let extension = 167 | let lastDot = unpacked.LastIndexOf '.' 168 | if lastDot = -1 then "" else unpacked.Substring lastDot 169 | let typemap = getExtensionMap settings 170 | 171 | match typemap.TryGetValue extension with 172 | | true, extval -> contextResponse.ContentType <- extval 173 | | false, _ when settings.AllowUnknonwnFiletypes -> () 174 | | _ -> 175 | contextResponse.StatusCode <- 415 176 | raise (ArgumentException("Invalid resource type", contextRequest.Path.ToString())) 177 | 178 | task { 179 | try 180 | // Most efficient: synchronous read since it's disk I/O bound anyway 181 | // and avoids async overhead for small files 182 | // (and there is no File.ReadAllBytesAsync in .NET Framework 4.8) 183 | let bytes = File.ReadAllBytes unpacked 184 | 185 | match bytes.LongLength < settings.MinimumSizeToCompress with 186 | | true -> 187 | return false, bytes 188 | | false -> 189 | let lastmodified = File.GetLastWriteTimeUtc(unpacked).ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", System.Globalization.CultureInfo.InvariantCulture) 190 | contextResponse.Headers.Add("Last-Modified", [|lastmodified|]) 191 | use ms = new MemoryStream(bytes, false) 192 | return 193 | if checkNoValidETag contextRequest contextResponse cancellationSrc ms then 194 | true, bytes 195 | else 196 | false, null 197 | with 198 | | :? FileNotFoundException -> 199 | contextResponse.StatusCode <- 404 200 | return false, null 201 | } 202 | let encodeFile (enc:SupportedEncodings) (settings:CompressionSettings) (contextRequest:IOwinRequest) (contextResponse:IOwinResponse) (cancellationSrc:Threading.CancellationTokenSource)= 203 | task { 204 | let cancellationToken = cancellationSrc.Token 205 | if cancellationToken.IsCancellationRequested then () 206 | let! awaited = getFile settings contextRequest contextResponse cancellationSrc 207 | let shouldProcess, bytes = awaited 208 | if not shouldProcess then 209 | if isNull bytes then 210 | return () 211 | else 212 | return! contextResponse.WriteAsync(bytes, cancellationToken) 213 | else 214 | 215 | use output = new MemoryStream() 216 | if not(contextResponse.Headers.ContainsKey "Vary") then 217 | contextResponse.Headers.Add("Vary", [|"Accept-Encoding"|]) 218 | use zipped = 219 | match enc with 220 | | Deflate -> 221 | contextResponse.Headers.Add("Content-Encoding", [|"deflate"|]) 222 | new DeflateStream(output, CompressionMode.Compress) :> Stream 223 | | GZip -> 224 | contextResponse.Headers.Add("Content-Encoding", [|"gzip"|]) 225 | new GZipStream(output, CompressionMode.Compress) :> Stream 226 | let! t1 = zipped.WriteAsync(bytes, 0, bytes.Length, cancellationToken) 227 | t1 |> ignore 228 | zipped.Close() 229 | output.Close() 230 | 231 | let op = output.ToArray() 232 | 233 | let doStream = 234 | if cancellationToken.IsCancellationRequested then 235 | false 236 | else 237 | try 238 | let canStream = String.Equals(contextRequest.Protocol, "HTTP/1.1", StringComparison.Ordinal) 239 | if canStream && (int64 defaultBufferSize) < op.LongLength then 240 | if not(contextResponse.Headers.ContainsKey("Transfer-Encoding")) 241 | || contextResponse.Headers.["Transfer-Encoding"] <> "chunked" then 242 | contextResponse.Headers.["Transfer-Encoding"] <- "chunked" 243 | true 244 | else 245 | if (not contextResponse.ContentLength.HasValue) || (contextResponse.ContentLength.Value <> -1 && contextResponse.ContentLength.Value <> op.LongLength) then 246 | contextResponse.ContentLength <- Nullable op.LongLength 247 | false 248 | 249 | with | _ -> 250 | false // Content length info is not so important... 251 | 252 | if doStream then 253 | return! zipped.CopyToAsync(contextResponse.Body, defaultBufferSize, cancellationToken) 254 | else 255 | return! contextResponse.WriteAsync(op, cancellationToken) 256 | } :> Task 257 | 258 | let compressableExtension (settings:CompressionSettings) (path:string) = 259 | match path with 260 | | null -> true 261 | | x -> 262 | let lastDot = x.LastIndexOf('.') 263 | if lastDot = -1 then false 264 | else 265 | let typemap = getExtensionMap settings 266 | typemap.ContainsKey(x.Substring(lastDot)) 267 | 268 | let encodeStream (enc:SupportedEncodings) (settings:CompressionSettings) (contextRequest:IOwinRequest) (contextResponse:IOwinResponse) (cancellationSrc:Threading.CancellationTokenSource) (next:Func) = 269 | 270 | let cancellationToken = cancellationSrc.Token 271 | let originalLengthNotEnough = contextResponse.Body.CanRead && contextResponse.Body.Length < settings.MinimumSizeToCompress 272 | let inline checkCompressability buffer = 273 | let inline captureResponse() = 274 | match buffer with 275 | | Some bufferStream -> 276 | contextResponse.Body <- bufferStream 277 | | None -> () 278 | let compressableExtension = compressableExtension settings (contextRequest.Path.ToString()) 279 | if compressableExtension then // non-stream, but Invoke can change "/" -> "index.html" 280 | captureResponse() 281 | true 282 | elif String.IsNullOrEmpty contextResponse.ContentType then 283 | if settings.AllowUnknonwnFiletypes then 284 | captureResponse() 285 | true 286 | else false 287 | else 288 | let contentType = 289 | // We are not interested of charset, etc - use StringComparison to avoid ToLower allocation 290 | let rawContentType = 291 | match contextResponse.ContentType.IndexOf(';') with 292 | | -1 -> contextResponse.ContentType 293 | | idx -> contextResponse.ContentType.Substring(0, idx) 294 | rawContentType 295 | if settings.AllowedExtensionAndMimeTypes 296 | |> Seq.map snd |> Seq.append ["text/html"] 297 | |> Seq.exists(fun mimeType -> String.Equals(mimeType, contentType, StringComparison.OrdinalIgnoreCase)) then 298 | captureResponse() 299 | true 300 | else 301 | false 302 | 303 | let isCompressable = 304 | (checkCompressability None) && not(settings.ExcludedPaths |> Seq.exists(fun p -> contextRequest.Path.ToString().Contains p)) 305 | && contextResponse.Body.CanWrite 306 | 307 | let inline continuation2 (pipedLengthNotEnough:bool) (copyBufferToBody:unit->Task) (copyBodyToCompressor:Stream->Task) (copyCompressedToBody:MemoryStream->Task) = 308 | task { 309 | 310 | let noCompression = 311 | (not contextResponse.Body.CanSeek) || (not contextResponse.Body.CanRead) 312 | || (originalLengthNotEnough && pipedLengthNotEnough) 313 | || (contextResponse.Headers.ContainsKey("Content-Encoding") && 314 | not(String.IsNullOrWhiteSpace(contextResponse.Headers.["Content-Encoding"]))) 315 | 316 | match noCompression with 317 | | true -> 318 | if contextResponse.Body.CanSeek then 319 | contextResponse.Body.Seek(0L, SeekOrigin.Begin) |> ignore 320 | do! copyBufferToBody() 321 | return () 322 | | false -> 323 | 324 | if not(contextResponse.Headers.ContainsKey "Vary") then 325 | contextResponse.Headers.Add("Vary", [|"Accept-Encoding"|]) 326 | 327 | use output = new MemoryStream() 328 | 329 | use zipped = 330 | match enc with 331 | | Deflate -> 332 | contextResponse.Headers.Add("Content-Encoding", [|"deflate"|]) 333 | new DeflateStream(output, CompressionMode.Compress) :> Stream 334 | | GZip -> 335 | contextResponse.Headers.Add("Content-Encoding", [|"gzip"|]) 336 | new GZipStream(output, CompressionMode.Compress) :> Stream 337 | //let! t1 = zipped.WriteAsync(bytes, 0, bytes.Length, cancellationToken) 338 | if contextResponse.Body.CanSeek then 339 | contextResponse.Body.Seek(0L, SeekOrigin.Begin) |> ignore 340 | 341 | do! copyBodyToCompressor(zipped) 342 | 343 | zipped.Close() 344 | output.Close() 345 | let op = output.ToArray() 346 | 347 | if not(cancellationToken.IsCancellationRequested) then 348 | try 349 | let canStream = String.Equals(contextRequest.Protocol, "HTTP/1.1", StringComparison.Ordinal) && not settings.StreamingDisabled 350 | if canStream && (int64 defaultBufferSize) < op.LongLength then 351 | if not(contextResponse.Headers.ContainsKey("Transfer-Encoding")) 352 | || contextResponse.Headers.["Transfer-Encoding"] <> "chunked" then 353 | contextResponse.Headers.["Transfer-Encoding"] <- "chunked" 354 | else 355 | if (not contextResponse.ContentLength.HasValue) || (contextResponse.ContentLength.Value <> -1 && contextResponse.ContentLength.Value <> op.LongLength) then 356 | contextResponse.ContentLength <- Nullable(op.LongLength) 357 | with | _ -> () // Content length info is not so important... 358 | 359 | use tmpOutput = new MemoryStream(op) 360 | if tmpOutput.CanSeek then 361 | tmpOutput.Seek(0L, SeekOrigin.Begin) |> ignore 362 | 363 | do! copyCompressedToBody tmpOutput 364 | return () 365 | } 366 | 367 | task { 368 | use streamWebOutput = contextResponse.Body 369 | use buffer = new MemoryStream() 370 | 371 | if isCompressable then 372 | contextResponse.Body <- buffer // stream 373 | else 374 | () 375 | 376 | do! next.Invoke() 377 | 378 | let pipedLengthNotEnough = 379 | contextResponse.Body.CanRead && 380 | (if contextResponse.Body.Length = 0 && contextResponse.Body = buffer && streamWebOutput.CanRead then streamWebOutput.Length else contextResponse.Body.Length) < settings.MinimumSizeToCompress 381 | 382 | let usecompress = isCompressable || checkCompressability (Some buffer) 383 | if usecompress && checkNoValidETag contextRequest contextResponse cancellationSrc (if contextResponse.Body.Length > 0L then contextResponse.Body else streamWebOutput) then 384 | 385 | let inline copyBufferToBody() = 386 | task { 387 | do! contextResponse.Body.CopyToAsync(streamWebOutput, defaultBufferSize, cancellationToken) 388 | contextResponse.Body <- streamWebOutput 389 | } :> Task 390 | let inline copyBodyToCompressor (zipped:Stream) = contextResponse.Body.CopyToAsync(zipped, defaultBufferSize, cancellationToken) 391 | let inline copyCompressedToBody (zippedData:MemoryStream) = 392 | task { 393 | if zippedData.Length = 0 && streamWebOutput.CanRead then 394 | 395 | use output = new MemoryStream() 396 | use zipped = 397 | match enc with 398 | | Deflate -> 399 | new DeflateStream(output, CompressionMode.Compress) :> Stream 400 | | GZip -> 401 | new GZipStream(output, CompressionMode.Compress) :> Stream 402 | 403 | if streamWebOutput.CanSeek then 404 | streamWebOutput.Seek(0L, SeekOrigin.Begin) |> ignore 405 | 406 | do! streamWebOutput.CopyToAsync(zipped, defaultBufferSize, cancellationToken) 407 | zipped.Close() 408 | contextResponse.Body <- output 409 | else 410 | do! zippedData.CopyToAsync(streamWebOutput, defaultBufferSize, cancellationToken) 411 | contextResponse.Body <- streamWebOutput 412 | } :> Task 413 | return! continuation2 pipedLengthNotEnough copyBufferToBody copyBodyToCompressor copyCompressedToBody 414 | 415 | else 416 | return () 417 | } :> Task 418 | 419 | 420 | let inline internal compress (context:IOwinContext) (settings:CompressionSettings) (mode:ResponseMode) = 421 | let cancellationSrc = System.Threading.CancellationTokenSource.CreateLinkedTokenSource(context.Request.CallCancelled) 422 | let cancellationToken = cancellationSrc.Token 423 | 424 | let encodings = 425 | if cancellationToken.IsCancellationRequested then "" 426 | else context.Request.Headers.["Accept-Encoding"] 427 | 428 | let inline encodeOutput (enc:SupportedEncodings) = 429 | 430 | match settings.CacheExpireTime with 431 | | ValueSome d when not (context.Response.Headers.IsReadOnly) -> context.Response.Expires <- Nullable(d) 432 | | _ -> () 433 | 434 | match mode with 435 | | File -> encodeFile enc settings context.Request context.Response cancellationSrc 436 | | ContextResponseBody(next) -> 437 | 438 | 439 | if cancellationToken.IsCancellationRequested then 440 | task { 441 | do! next.Invoke() 442 | return () 443 | } 444 | else 445 | 446 | encodeStream enc settings context.Request context.Response cancellationSrc next 447 | 448 | let inline encodeTask() = 449 | let inline writeAsyncContext() = 450 | match mode with 451 | | File -> 452 | task { 453 | let! comp, r = getFile settings context.Request context.Response cancellationSrc 454 | if comp then 455 | return! context.Response.WriteAsync(r, cancellationToken) 456 | else return! Task.Delay 50 457 | } :> Task 458 | | ContextResponseBody(next) -> 459 | next.Invoke() 460 | if String.IsNullOrEmpty encodings then writeAsyncContext() 461 | elif encodings.Contains "deflate" && not(settings.DeflateDisabled) then encodeOutput Deflate 462 | elif encodings.Contains "gzip" then encodeOutput GZip 463 | else writeAsyncContext() 464 | 465 | encodeTask 466 | 467 | open OwinCompression 468 | 469 | [] 470 | type CompressionExtensions = 471 | 472 | [] 473 | static member UseCompressionModule(app:IAppBuilder, settings:CompressionSettings) = 474 | app.Use(fun context next -> 475 | (Internals.compress context settings (ResponseMode.ContextResponseBody next) )() 476 | ) 477 | 478 | [] 479 | static member UseCompressionModule(app:IAppBuilder) = 480 | CompressionExtensions.UseCompressionModule(app, DefaultCompressionSettings) 481 | 482 | /// You can set a path, which is the URL that will be captured. 483 | /// The subsequent url-path will be mapped to the server path. 484 | [] 485 | static member MapCompressionModule(app:IAppBuilder, path:string, settings:CompressionSettings) = 486 | app.Map(path, fun ap -> 487 | ap.Run(fun context -> 488 | (Internals.compress context settings ResponseMode.File)() 489 | )) 490 | 491 | /// You can set a path, which is the URL that will be captured. 492 | /// The subsequent url-path will be mapped to the server path. 493 | /// Uses OwinCompression.DefaultCompressionSettings 494 | [] 495 | static member MapCompressionModule(app:IAppBuilder, path:string) = 496 | CompressionExtensions.MapCompressionModule(app, path, DefaultCompressionSettings) 497 | -------------------------------------------------------------------------------- /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 | #if NETSTANDARD21 27 | | Brotli 28 | #endif 29 | 30 | /// Do you fetch files or do you encode context.Response.Body? 31 | type ResponseMode = 32 | | File 33 | | ContextResponseBody of Next: Func 34 | 35 | /// Settings for compression. 36 | type CompressionSettings = { 37 | ServerPath: string; 38 | AllowUnknonwnFiletypes: bool; 39 | AllowRootDirectories: bool; 40 | CacheExpireTime: DateTimeOffset voption; 41 | AllowedExtensionAndMimeTypes: IEnumerable; 42 | MinimumSizeToCompress: int64; 43 | DeflateDisabled: bool; 44 | #if NETSTANDARD21 45 | BrotliDisabled: bool; 46 | #endif 47 | StreamingDisabled: bool; 48 | ExcludedPaths: IEnumerable; 49 | } 50 | 51 | module OwinCompression = 52 | #if INTERACTIVE 53 | let basePath = __SOURCE_DIRECTORY__; 54 | #else 55 | let basePath = System.Reflection.Assembly.GetExecutingAssembly().Location |> Path.GetDirectoryName 56 | #endif 57 | /// Default settings for compression. 58 | let DefaultCompressionSettings = { 59 | ServerPath = basePath; 60 | AllowUnknonwnFiletypes = false; 61 | AllowRootDirectories = false; 62 | CacheExpireTime = ValueNone 63 | MinimumSizeToCompress = 1000L 64 | DeflateDisabled = false 65 | BrotliDisabled = false 66 | StreamingDisabled = false 67 | ExcludedPaths = [| "/signalr/" |] 68 | AllowedExtensionAndMimeTypes = 69 | [| 70 | ".js" , "application/javascript"; 71 | ".css" , "text/css"; 72 | ".yml" , "application/x-yaml"; 73 | ".json" , "application/json"; 74 | ".svg" , "image/svg+xml"; 75 | ".txt" , "text/plain"; 76 | ".html" , "application/json"; // we don't want to follow hyperlinks, so not "text/html" 77 | ".map" , "application/json"; 78 | ".ttf" , "application/x-font-ttf"; 79 | ".otf" , "application/x-font-opentype"; 80 | ".ico" , "image/x-icon"; 81 | ".xml" , "application/xml"; 82 | ".xsl" , "text/xml"; 83 | ".xhtml", "application/xhtml+xml"; 84 | ".rss" , "application/rss+xml"; 85 | ".eot" , "font/eot"; 86 | ".aspx" , "text/html"; 87 | |] 88 | } 89 | 90 | /// Default settings with custom path. No cache time. 91 | let DefaultCompressionSettingsWithPath path = 92 | {DefaultCompressionSettings with 93 | ServerPath = path; CacheExpireTime = ValueSome (DateTimeOffset.Now.AddDays 7.) } 94 | 95 | /// Default settings with custom path and cache-time. C#-helper method. 96 | let DefaultCompressionSettingsWithPathAndCache(path,cachetime) = 97 | {DefaultCompressionSettings with ServerPath = path; CacheExpireTime = ValueSome (cachetime) } 98 | 99 | let private defaultBufferSize = 81920 100 | 101 | /// Cached extension to MIME type mapping for performance 102 | let private getExtensionMap (settings:CompressionSettings) = 103 | // Cache the map computation to avoid repeated Map.ofSeq operations 104 | settings.AllowedExtensionAndMimeTypes |> Map.ofSeq 105 | 106 | module Internals = 107 | 108 | // Use thread-safe MD5 computation for better performance 109 | let private computeHash (item:Stream) = 110 | use md5 = System.Security.Cryptography.MD5.Create() 111 | md5.ComputeHash item 112 | 113 | let getHash (item:Stream) = 114 | if item.CanRead then 115 | let hasPos = 116 | if item.CanSeek && item.Position > 0L then 117 | let tmp = item.Position 118 | item.Position <- 0L 119 | ValueSome tmp 120 | else ValueNone 121 | let hashBytes = computeHash item 122 | let res = BitConverter.ToString(hashBytes).Replace("-","") 123 | match hasPos with 124 | | ValueSome x when item.CanSeek -> item.Position <- x 125 | | _ -> () 126 | ValueSome res 127 | else ValueNone 128 | 129 | let inline create304Response (contextResponse:HttpResponse) = 130 | if not contextResponse.HasStarted then 131 | if contextResponse.StatusCode <> 304 then 132 | contextResponse.StatusCode <- 304 133 | contextResponse.Body.Close() 134 | contextResponse.Body <- Stream.Null 135 | if contextResponse.ContentLength.HasValue then 136 | contextResponse.ContentLength <- Nullable() 137 | false 138 | 139 | let checkNoValidETag (contextRequest:HttpRequest) (contextResponse:HttpResponse) (cancellationSrc:Threading.CancellationTokenSource) (itemToCheck:Stream) = 140 | let ifNoneMatch, noneMatchValue = 141 | match contextRequest.Headers.TryGetValue "If-None-Match" with 142 | | true, nonem when nonem <> StringValues.Empty -> true, nonem 143 | | _ -> false, StringValues.Empty 144 | 145 | let hasNoPragma = 146 | match contextRequest.Headers.TryGetValue "Pragma" with 147 | | false, _ -> true 148 | | true, x when x <> StringValues("no-cache") -> true 149 | | true, _ -> false 150 | 151 | let etagVal = 152 | match contextResponse.Headers.TryGetValue "ETag" with 153 | | true, etag -> etag 154 | | false, _ -> StringValues.Empty 155 | if ifNoneMatch && hasNoPragma then 156 | if noneMatchValue = etagVal then 157 | if not (isNull cancellationSrc) then cancellationSrc.Cancel() 158 | create304Response contextResponse 159 | else 160 | 161 | match getHash itemToCheck with 162 | | ValueSome etag -> 163 | if noneMatchValue = StringValues etag then 164 | if not (isNull cancellationSrc) then cancellationSrc.Cancel() 165 | create304Response contextResponse 166 | else 167 | if etagVal = StringValues.Empty && 168 | not contextResponse.Headers.IsReadOnly then 169 | contextResponse.Headers.["ETag"] <- StringValues etag 170 | true 171 | | ValueNone -> true 172 | else 173 | if etagVal = StringValues.Empty then 174 | match getHash(itemToCheck) with 175 | | ValueSome etag when not contextResponse.Headers.IsReadOnly && itemToCheck.Length > 0L -> 176 | contextResponse.Headers.["ETag"] <- StringValues etag 177 | | _ -> () 178 | true 179 | 180 | let internal getFile (settings:CompressionSettings) (contextRequest:HttpRequest) (contextResponse:HttpResponse) (cancellationSrc:Threading.CancellationTokenSource) = 181 | let unpacked :string = 182 | let p = contextRequest.Path.ToString() 183 | if not(settings.AllowRootDirectories) && p.Contains ".." then failwith "Invalid path" 184 | if File.Exists p then failwith "Invalid resource" 185 | let p2 = 186 | #if NETSTANDARD21 187 | match p.StartsWith '/' with 188 | #else 189 | match p.StartsWith "/" with 190 | #endif 191 | | true -> p.Substring 1 192 | | false -> p 193 | Path.Combine ([| settings.ServerPath; p2|]) 194 | 195 | let extension = 196 | let lastDot = unpacked.LastIndexOf '.' 197 | if lastDot = -1 then "" else unpacked.Substring lastDot 198 | let typemap = getExtensionMap settings 199 | 200 | match typemap.TryGetValue extension with 201 | | true, extval -> contextResponse.ContentType <- extval 202 | | false, _ when settings.AllowUnknonwnFiletypes -> () 203 | | _ -> 204 | if not contextResponse.HasStarted then 205 | contextResponse.StatusCode <- 415 206 | raise (ArgumentException("Invalid resource type", contextRequest.Path.ToString())) 207 | 208 | task { 209 | try 210 | let! bytes = File.ReadAllBytesAsync(unpacked, cancellationSrc.Token) 211 | match FileInfo(unpacked).Length < settings.MinimumSizeToCompress with 212 | | true -> 213 | return false, bytes 214 | | false -> 215 | let lastmodified = File.GetLastWriteTimeUtc(unpacked).ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", System.Globalization.CultureInfo.InvariantCulture) 216 | contextResponse.Headers.Add("Last-Modified", StringValues(lastmodified)) 217 | use ms = new MemoryStream(bytes, false) 218 | return 219 | if checkNoValidETag contextRequest contextResponse cancellationSrc ms then 220 | true, bytes 221 | else 222 | false, null 223 | with 224 | | :? FileNotFoundException -> 225 | if not contextResponse.HasStarted then 226 | contextResponse.StatusCode <- 404 227 | return false, null 228 | } 229 | 230 | let encodeFile (enc:SupportedEncodings) (settings:CompressionSettings) (contextRequest:HttpRequest) (contextResponse:HttpResponse) (cancellationSrc:Threading.CancellationTokenSource) = 231 | task { 232 | let cancellationToken = cancellationSrc.Token 233 | if cancellationToken.IsCancellationRequested then () 234 | let! awaited = getFile settings contextRequest contextResponse cancellationSrc 235 | let shouldProcess, bytes = awaited 236 | if not shouldProcess then 237 | if isNull bytes then 238 | return () 239 | else 240 | #if NETSTANDARD21 241 | return! contextResponse.Body.WriteAsync(bytes.AsMemory(0, bytes.Length), cancellationToken) 242 | #else 243 | return! contextResponse.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken) 244 | #endif 245 | else 246 | 247 | use output = new MemoryStream() 248 | if not(contextResponse.Headers.ContainsKey "Vary") then 249 | contextResponse.Headers.Add("Vary", StringValues("Accept-Encoding")) 250 | use zipped = 251 | match enc with 252 | | Deflate -> 253 | contextResponse.Headers.Add("Content-Encoding", StringValues("deflate")) 254 | new DeflateStream(output, CompressionMode.Compress) :> Stream 255 | | GZip -> 256 | contextResponse.Headers.Add("Content-Encoding", StringValues("gzip")) 257 | new GZipStream(output, CompressionMode.Compress) :> Stream 258 | #if NETSTANDARD21 259 | | Brotli -> 260 | contextResponse.Headers.Add("Content-Encoding", StringValues("br")) 261 | new BrotliStream(output, CompressionMode.Compress) :> Stream 262 | #endif 263 | #if NETSTANDARD21 264 | let! t1 = zipped.WriteAsync(bytes.AsMemory(0, bytes.Length), cancellationToken) 265 | #else 266 | let! t1 = zipped.WriteAsync(bytes, 0, bytes.Length, cancellationToken) 267 | #endif 268 | t1 |> ignore 269 | zipped.Close() 270 | output.Close() 271 | let op = output.ToArray() 272 | 273 | let doStream = 274 | if cancellationToken.IsCancellationRequested then 275 | false 276 | else 277 | try 278 | let canStream = String.Equals(contextRequest.Protocol, "HTTP/1.1", StringComparison.Ordinal) 279 | if canStream && (int64 defaultBufferSize) < op.LongLength then 280 | if not(contextResponse.Headers.ContainsKey("Transfer-Encoding")) 281 | || contextResponse.Headers.["Transfer-Encoding"] <> StringValues("chunked") then 282 | contextResponse.Headers.["Transfer-Encoding"] <- "chunked" 283 | true 284 | else 285 | if (not contextResponse.ContentLength.HasValue) || (contextResponse.ContentLength.Value <> -1 && contextResponse.ContentLength.Value <> op.LongLength) then 286 | contextResponse.ContentLength <- Nullable op.LongLength 287 | false 288 | 289 | with | _ -> 290 | false // Content length info is not so important... 291 | 292 | if doStream then 293 | return! zipped.CopyToAsync(contextResponse.Body, defaultBufferSize, cancellationToken) 294 | else 295 | #if NETSTANDARD21 296 | return! contextResponse.Body.WriteAsync(op.AsMemory(0, op.Length), cancellationToken) 297 | #else 298 | return! contextResponse.Body.WriteAsync(op, 0, op.Length, cancellationToken) 299 | #endif 300 | } :> Task 301 | 302 | 303 | let compressableExtension (settings:CompressionSettings) (path:string) = 304 | match path with 305 | | null -> true 306 | | x -> 307 | let lastDot = x.LastIndexOf('.') 308 | if lastDot = -1 then false 309 | else 310 | let typemap = getExtensionMap settings 311 | typemap.ContainsKey(x.Substring(lastDot)) 312 | 313 | let encodeStream (enc:SupportedEncodings) (settings:CompressionSettings) (contextRequest:HttpRequest) (contextResponse:HttpResponse) (cancellationSrc:Threading.CancellationTokenSource) (next:Func) = 314 | let cancellationToken = cancellationSrc.Token 315 | let originalLengthNotEnough = contextResponse.Body.CanRead && contextResponse.Body.Length < settings.MinimumSizeToCompress 316 | let inline checkCompressability buffer = 317 | let inline captureResponse() = 318 | match buffer with 319 | | Some bufferStream -> 320 | contextResponse.Body <- bufferStream 321 | | None -> () 322 | let compressableExtension = compressableExtension settings (contextRequest.Path.ToString()) 323 | if compressableExtension then // non-stream, but Invoke can change "/" -> "index.html" 324 | captureResponse() 325 | true 326 | elif String.IsNullOrEmpty contextResponse.ContentType then 327 | if settings.AllowUnknonwnFiletypes then 328 | captureResponse() 329 | true 330 | else false 331 | else 332 | let contentType = 333 | // We are not interested of charset, etc - use IndexOf to avoid ToLower allocation 334 | let rawContentType = 335 | match contextResponse.ContentType.IndexOf(';') with 336 | | -1 -> contextResponse.ContentType 337 | | idx -> contextResponse.ContentType.Substring(0, idx) 338 | rawContentType 339 | if settings.AllowedExtensionAndMimeTypes 340 | |> Seq.map snd |> Seq.append ["text/html"] 341 | |> Seq.exists(fun mimeType -> String.Equals(mimeType, contentType, StringComparison.OrdinalIgnoreCase)) then 342 | captureResponse() 343 | true 344 | else 345 | false 346 | 347 | let isCompressable = 348 | (checkCompressability None) && not(settings.ExcludedPaths |> Seq.exists(fun p -> contextRequest.Path.ToString().Contains p)) 349 | && contextResponse.Body.CanWrite 350 | 351 | let inline continuation2 (pipedLengthNotEnough:bool) (copyBufferToBody:unit->Task) (copyBodyToCompressor:Stream->Task) (copyCompressedToBody:MemoryStream->Task) = 352 | task { 353 | 354 | let noCompression = 355 | (not contextResponse.Body.CanSeek) || (not contextResponse.Body.CanRead) 356 | || (originalLengthNotEnough && pipedLengthNotEnough) 357 | || (contextResponse.Headers.ContainsKey("Content-Encoding") && 358 | not(String.IsNullOrWhiteSpace(contextResponse.Headers.["Content-Encoding"]))) 359 | 360 | match noCompression with 361 | | true -> 362 | if contextResponse.Body.CanSeek then 363 | contextResponse.Body.Seek(0L, SeekOrigin.Begin) |> ignore 364 | do! copyBufferToBody() 365 | return () 366 | | false -> 367 | 368 | if not(contextResponse.Headers.ContainsKey "Vary") then 369 | contextResponse.Headers.Add("Vary", StringValues("Accept-Encoding")) 370 | 371 | use output = new MemoryStream() 372 | 373 | use zipped = 374 | match enc with 375 | | Deflate -> 376 | contextResponse.Headers.Add("Content-Encoding", StringValues("deflate")) 377 | new DeflateStream(output, CompressionMode.Compress) :> Stream 378 | | GZip -> 379 | contextResponse.Headers.Add("Content-Encoding", StringValues("gzip")) 380 | new GZipStream(output, CompressionMode.Compress) :> Stream 381 | #if NETSTANDARD21 382 | | Brotli -> 383 | contextResponse.Headers.Add("Content-Encoding", StringValues("br")) 384 | new BrotliStream(output, CompressionMode.Compress) :> Stream 385 | #endif 386 | //let! t1 = zipped.WriteAsync(bytes, 0, bytes.Length, cancellationToken) 387 | if contextResponse.Body.CanSeek then 388 | contextResponse.Body.Seek(0L, SeekOrigin.Begin) |> ignore 389 | 390 | do! copyBodyToCompressor(zipped) 391 | 392 | zipped.Close() 393 | output.Close() 394 | let op = output.ToArray() 395 | 396 | if not(cancellationToken.IsCancellationRequested) then 397 | try 398 | let canStream = String.Equals(contextRequest.Protocol, "HTTP/1.1", StringComparison.Ordinal) && not settings.StreamingDisabled 399 | if canStream && (int64 defaultBufferSize) < op.LongLength then 400 | if not(contextResponse.Headers.ContainsKey("Transfer-Encoding")) 401 | || contextResponse.Headers.["Transfer-Encoding"] <> StringValues("chunked") then 402 | contextResponse.Headers.["Transfer-Encoding"] <- StringValues("chunked") 403 | else 404 | if (not contextResponse.ContentLength.HasValue) || (contextResponse.ContentLength.Value <> -1 && contextResponse.ContentLength.Value <> op.LongLength) then 405 | contextResponse.ContentLength <- Nullable(op.LongLength) 406 | with | _ -> () // Content length info is not so important... 407 | 408 | use tmpOutput = new MemoryStream(op) 409 | if tmpOutput.CanSeek then 410 | tmpOutput.Seek(0L, SeekOrigin.Begin) |> ignore 411 | 412 | do! copyCompressedToBody tmpOutput 413 | return () 414 | } 415 | task { 416 | use streamWebOutput = contextResponse.Body 417 | use buffer = new MemoryStream() 418 | 419 | if isCompressable then 420 | contextResponse.Body <- buffer // stream 421 | else 422 | () 423 | 424 | do! next.Invoke() 425 | 426 | let pipedLengthNotEnough = 427 | contextResponse.Body.CanRead && 428 | (if contextResponse.Body.Length = 0 && contextResponse.Body = buffer && streamWebOutput.CanRead then streamWebOutput.Length else contextResponse.Body.Length) < settings.MinimumSizeToCompress 429 | 430 | let usecompress = isCompressable || checkCompressability (Some buffer) 431 | if usecompress && checkNoValidETag contextRequest contextResponse cancellationSrc contextResponse.Body then 432 | let inline copyBufferToBody() = 433 | task { 434 | do! contextResponse.Body.CopyToAsync(streamWebOutput, defaultBufferSize, cancellationToken) 435 | contextResponse.Body <- streamWebOutput 436 | } :> Task 437 | let inline copyBodyToCompressor (zipped:Stream) = contextResponse.Body.CopyToAsync(zipped, defaultBufferSize, cancellationToken) 438 | let inline copyCompressedToBody (zippedData:MemoryStream) = 439 | task { 440 | if zippedData.Length = 0 && streamWebOutput.CanRead then 441 | use output = new MemoryStream() 442 | use zipped = 443 | match enc with 444 | | Deflate -> 445 | new DeflateStream(output, CompressionMode.Compress) :> Stream 446 | | GZip -> 447 | new GZipStream(output, CompressionMode.Compress) :> Stream 448 | #if NETSTANDARD21 449 | | Brotli -> 450 | new BrotliStream(output, CompressionMode.Compress) :> Stream 451 | #endif 452 | 453 | if streamWebOutput.CanSeek then 454 | streamWebOutput.Seek(0L, SeekOrigin.Begin) |> ignore 455 | 456 | do! streamWebOutput.CopyToAsync(zipped, defaultBufferSize, cancellationToken) 457 | zipped.Close() 458 | contextResponse.Body <- output 459 | else 460 | do! zippedData.CopyToAsync(streamWebOutput, defaultBufferSize, cancellationToken) 461 | contextResponse.Body <- streamWebOutput 462 | } :> Task 463 | return! continuation2 pipedLengthNotEnough copyBufferToBody copyBodyToCompressor copyCompressedToBody 464 | else 465 | return () 466 | } :> Task 467 | 468 | let inline internal compress (context:HttpContext) (settings:CompressionSettings) (mode:ResponseMode) = 469 | let cancellationSrc = System.Threading.CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted) 470 | let cancellationToken = cancellationSrc.Token 471 | 472 | let encodings = 473 | if cancellationToken.IsCancellationRequested then "" 474 | else 475 | match context.Request.Headers.TryGetValue "Accept-Encoding" with 476 | | true, x -> x.ToString() 477 | | false, _ -> "" 478 | let inline encodeOutput (enc:SupportedEncodings) = 479 | 480 | match settings.CacheExpireTime with 481 | | ValueSome d when not (context.Response.Headers.IsReadOnly) -> context.Response.Headers.["Expires"] <- StringValues(d.ToString()) 482 | | _ -> () 483 | 484 | match mode with 485 | | File -> encodeFile enc settings context.Request context.Response cancellationSrc 486 | | ContextResponseBody(next) -> 487 | 488 | if cancellationToken.IsCancellationRequested then 489 | task { 490 | do! next.Invoke() 491 | return () 492 | } 493 | else 494 | encodeStream enc settings context.Request context.Response cancellationSrc next 495 | 496 | let inline encodeTask() = 497 | let inline writeAsyncContext() = 498 | match mode with 499 | | File -> 500 | task { 501 | let! comp, r = getFile settings context.Request context.Response cancellationSrc 502 | if comp then 503 | #if NETSTANDARD21 504 | return! context.Response.Body.WriteAsync(r.AsMemory(0, r.Length), cancellationToken) 505 | #else 506 | return! context.Response.Body.WriteAsync(r, 0, r.Length, cancellationToken) 507 | #endif 508 | else return! Task.Delay 50 509 | } :> Task 510 | | ContextResponseBody(next) -> 511 | next.Invoke() 512 | if String.IsNullOrEmpty encodings then writeAsyncContext() 513 | #if NETSTANDARD21 514 | elif encodings.Contains "br" && not(settings.BrotliDisabled) then encodeOutput Brotli 515 | #endif 516 | elif encodings.Contains "gzip" then encodeOutput GZip 517 | elif encodings.Contains "deflate" && not(settings.DeflateDisabled) then encodeOutput Deflate 518 | else writeAsyncContext() 519 | 520 | encodeTask 521 | 522 | open OwinCompression 523 | 524 | [] 525 | type CompressionExtensions = 526 | 527 | [] 528 | static member UseCompressionModule(app:IApplicationBuilder, settings:CompressionSettings) = 529 | app.Use(fun context next -> 530 | (Internals.compress context settings (ResponseMode.ContextResponseBody next) )() 531 | ) 532 | 533 | [] 534 | static member UseCompressionModule(app:IApplicationBuilder) = 535 | CompressionExtensions.UseCompressionModule(app, DefaultCompressionSettings) 536 | 537 | /// You can set a path, which is the URL that will be captured. 538 | /// The subsequent url-path will be mapped to the server path. 539 | [] 540 | static member MapCompressionModule(app:IApplicationBuilder, path:PathString, settings:CompressionSettings) = 541 | app.Map(path, fun ap -> 542 | ap.Run(fun context -> 543 | (Internals.compress context settings ResponseMode.File)() 544 | )) 545 | 546 | [] 547 | static member UseCompressionModuleLogTime(app:IApplicationBuilder, settings:CompressionSettings) = 548 | app.Use(fun context next -> 549 | task { 550 | let sw = System.Diagnostics.Stopwatch.StartNew() 551 | let! r = (Internals.compress context settings (ResponseMode.ContextResponseBody next) )() 552 | sw.Stop() 553 | let measure = "Took " + sw.Elapsed.TotalMilliseconds.ToString() 554 | System.Diagnostics.Debug.WriteLine measure 555 | return r 556 | } :> Task 557 | ) 558 | 559 | /// You can set a path, which is the URL that will be captured. 560 | /// The subsequent url-path will be mapped to the server path. 561 | /// Uses OwinCompression.DefaultCompressionSettings 562 | [] 563 | static member MapCompressionModule(app:IApplicationBuilder, path:PathString) = 564 | CompressionExtensions.MapCompressionModule(app, path, DefaultCompressionSettings) 565 | -------------------------------------------------------------------------------- /.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 | --------------------------------------------------------------------------------