├── .gitattributes
├── .github
├── FUNDING.yml
└── workflows
│ ├── codeql-analysis.yml
│ └── test.yml
├── .gitignore
├── CompressedStaticFiles.Tests
├── CompressedStaticFileMiddlewareTests.cs
├── CompressedStaticFiles.Tests.csproj
├── ImageCompressedStaticFileMiddlewareTests.cs
└── wwwroot
│ ├── IMG_6067.avif
│ ├── IMG_6067.jpg
│ ├── IMG_6067.webp
│ ├── favicon.avif
│ ├── favicon.ico
│ ├── favicon.ico.br
│ ├── favicon.ico.gz
│ ├── favicon.png
│ ├── favicon.webp
│ ├── generate_highquality.bat
│ ├── highquality.avif
│ ├── highquality.jpg
│ ├── highquality.png
│ ├── highquality.webp
│ ├── i_also_exist_compressed.html
│ ├── i_also_exist_compressed.html.br
│ ├── i_also_exist_compressed.html.gz
│ ├── i_am_smaller_in_uncompressed.html
│ ├── i_am_smaller_in_uncompressed.html.br
│ ├── i_exist_only_uncompressed.html
│ ├── icon.avif
│ ├── icon.png
│ ├── icon.webp
│ ├── with spaces.html
│ ├── with spaces.html.br
│ └── with spaces.html.gz
├── CompressedStaticFiles.sln
├── CompressedStaticFiles
├── AlternativeImageFile.cs
├── AlternativeImageFileProvider.cs
├── CompressedAlternativeFile.cs
├── CompressedAlternativeFileProvider.cs
├── CompressedStaticFileExtensions.cs
├── CompressedStaticFileMiddleware.cs
├── CompressedStaticFileOptions.cs
├── CompressedStaticFiles.csproj
├── IAlternativeFileProvider.cs
├── IFileAlternative.cs
├── LoggerExtensions.cs
└── icon.png
├── Example
├── Example.csproj
├── Program.cs
├── Properties
│ └── launchSettings.json
├── Startup.cs
├── appsettings.Development.json
├── appsettings.json
├── gulpfile.js
├── images
│ ├── IMG_6067.jpg
│ ├── favicon.ico
│ ├── favicon.png
│ └── icon.png
├── package-lock.json
├── package.json
└── wwwroot
│ ├── IMG_6067.avif
│ ├── IMG_6067.jpg
│ ├── IMG_6067.webp
│ ├── cover.css
│ ├── favicon.ico
│ ├── favicon.png
│ ├── favicon.webp
│ ├── icon.png
│ ├── icon.webp
│ ├── index.html
│ └── style.css
├── LICENSE
├── README.md
├── global.json
└── icon.svg
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: AnderssonPeter
2 | custom: ['https://www.paypal.com/donate?business=USVBQ3MG9HFLQ']
3 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '19 6 * * 4'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'csharp' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v2
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: run unit tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 | env:
10 | config: 'Release'
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Setup .NET5
15 | uses: actions/setup-dotnet@v1
16 | with:
17 | dotnet-version: 6.0.*
18 | - name: Install dependencies
19 | run: dotnet restore --verbosity normal
20 | - name: Build
21 | run: dotnet build --configuration $config --no-restore
22 | - name: Test
23 | run: dotnet test --configuration $config --no-restore --no-build --verbosity normal --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=lcov
24 | - name: Find lcov file path
25 | id: find-lcov-file-path
26 | shell: pwsh
27 | run: |
28 | $FilePath = (Get-ChildItem CompressedStaticFiles.Tests\TestResults\* | Select-Object -First 1 | Get-ChildItem).FullName
29 | Write-Host ::set-output name=path::$FilePath
30 | - name: Publish coverage report to coveralls.io
31 | uses: coverallsapp/github-action@master
32 | with:
33 | github-token: ${{ secrets.GITHUB_TOKEN }}
34 | path-to-lcov: ${{ steps.find-lcov-file-path.outputs.path }}
35 |
--------------------------------------------------------------------------------
/.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 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | [Xx]64/
19 | [Xx]86/
20 | [Bb]uild/
21 | bld/
22 | [Bb]in/
23 | [Oo]bj/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 |
85 | # Visual Studio profiler
86 | *.psess
87 | *.vsp
88 | *.vspx
89 | *.sap
90 |
91 | # TFS 2012 Local Workspace
92 | $tf/
93 |
94 | # Guidance Automation Toolkit
95 | *.gpState
96 |
97 | # ReSharper is a .NET coding add-in
98 | _ReSharper*/
99 | *.[Rr]e[Ss]harper
100 | *.DotSettings.user
101 |
102 | # JustCode is a .NET coding add-in
103 | .JustCode
104 |
105 | # TeamCity is a build add-in
106 | _TeamCity*
107 |
108 | # DotCover is a Code Coverage Tool
109 | *.dotCover
110 |
111 | # NCrunch
112 | _NCrunch_*
113 | .*crunch*.local.xml
114 | nCrunchTemp_*
115 |
116 | # MightyMoose
117 | *.mm.*
118 | AutoTest.Net/
119 |
120 | # Web workbench (sass)
121 | .sass-cache/
122 |
123 | # Installshield output folder
124 | [Ee]xpress/
125 |
126 | # DocProject is a documentation generator add-in
127 | DocProject/buildhelp/
128 | DocProject/Help/*.HxT
129 | DocProject/Help/*.HxC
130 | DocProject/Help/*.hhc
131 | DocProject/Help/*.hhk
132 | DocProject/Help/*.hhp
133 | DocProject/Help/Html2
134 | DocProject/Help/html
135 |
136 | # Click-Once directory
137 | publish/
138 |
139 | # Publish Web Output
140 | *.[Pp]ublish.xml
141 | *.azurePubxml
142 |
143 | # TODO: Un-comment the next line if you do not want to checkin
144 | # your web deploy settings because they may include unencrypted
145 | # passwords
146 | #*.pubxml
147 | *.publishproj
148 |
149 | # NuGet Packages
150 | *.nupkg
151 | # The packages folder can be ignored because of Package Restore
152 | **/packages/*
153 | # except build/, which is used as an MSBuild target.
154 | !**/packages/build/
155 | # Uncomment if necessary however generally it will be regenerated when needed
156 | #!**/packages/repositories.config
157 | # NuGet v3's project.json files produces more ignoreable files
158 | *.nuget.props
159 | *.nuget.targets
160 |
161 | # Microsoft Azure Build Output
162 | csx/
163 | *.build.csdef
164 |
165 | # Microsoft Azure Emulator
166 | ecf/
167 | rcf/
168 |
169 | # Microsoft Azure ApplicationInsights config file
170 | ApplicationInsights.config
171 |
172 | # Windows Store app package directory
173 | AppPackages/
174 | BundleArtifacts/
175 |
176 | # Visual Studio cache files
177 | # files ending in .cache can be ignored
178 | *.[Cc]ache
179 | # but keep track of directories ending in .cache
180 | !*.[Cc]ache/
181 |
182 | # Others
183 | ClientBin/
184 | [Ss]tyle[Cc]op.*
185 | ~$*
186 | *~
187 | *.dbmdl
188 | *.dbproj.schemaview
189 | *.pfx
190 | *.publishsettings
191 | node_modules/
192 | orleans.codegen.cs
193 |
194 | # RIA/Silverlight projects
195 | Generated_Code/
196 |
197 | # Backup & report files from converting an old project file
198 | # to a newer Visual Studio version. Backup files are not needed,
199 | # because we have git ;-)
200 | _UpgradeReport_Files/
201 | Backup*/
202 | UpgradeLog*.XML
203 | UpgradeLog*.htm
204 |
205 | # SQL Server files
206 | *.mdf
207 | *.ldf
208 |
209 | # Business Intelligence projects
210 | *.rdl.data
211 | *.bim.layout
212 | *.bim_*.settings
213 |
214 | # Microsoft Fakes
215 | FakesAssemblies/
216 |
217 | # GhostDoc plugin setting file
218 | *.GhostDoc.xml
219 |
220 | # Node.js Tools for Visual Studio
221 | .ntvs_analysis.dat
222 |
223 | # Visual Studio 6 build log
224 | *.plg
225 |
226 | # Visual Studio 6 workspace options file
227 | *.opt
228 |
229 | # Visual Studio LightSwitch build output
230 | **/*.HTMLClient/GeneratedArtifacts
231 | **/*.DesktopClient/GeneratedArtifacts
232 | **/*.DesktopClient/ModelManifest.xml
233 | **/*.Server/GeneratedArtifacts
234 | **/*.Server/ModelManifest.xml
235 | _Pvt_Extensions
236 |
237 | # LightSwitch generated files
238 | GeneratedArtifacts/
239 | ModelManifest.xml
240 |
241 | # Paket dependency manager
242 | .paket/paket.exe
243 |
244 | # FAKE - F# Make
245 | .fake/
246 |
247 | # Cake (C# Make)
248 | /tools
249 |
250 | # Example
251 | /Example/wwwroot/*.gz
252 | /Example/wwwroot/*.br
253 | /Example/wwwroot/*.min.*
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/CompressedStaticFileMiddlewareTests.cs:
--------------------------------------------------------------------------------
1 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
2 |
3 | using FluentAssertions;
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.AspNetCore.TestHost;
7 | using Microsoft.Extensions.FileProviders;
8 | using NSubstitute;
9 | using System;
10 | using System.Collections.Generic;
11 | using System.IO;
12 | using System.Linq;
13 | using System.Net;
14 | using System.Text;
15 | using System.Threading.Tasks;
16 | using Xunit;
17 |
18 | namespace CompressedStaticFiles.Tests
19 | {
20 | public class CompressedStaticFileMiddlewareTests
21 | {
22 |
23 | ///
24 | /// Call the next middleware if no matching file is found.
25 | ///
26 | ///
27 | [Fact]
28 | public async Task CallNextMiddleware()
29 | {
30 | // Arrange
31 | var builder = new WebHostBuilder()
32 | .ConfigureServices(sp =>
33 | {
34 | sp.AddCompressedStaticFiles();
35 | })
36 | .Configure(app =>
37 | {
38 | app.UseCompressedStaticFiles();
39 | app.Use(next =>
40 | {
41 | return async context =>
42 | {
43 | context.Response.StatusCode = 999;
44 | };
45 | });
46 | });
47 | var server = new TestServer(builder);
48 |
49 | // Act
50 | var response = await server.CreateClient().GetAsync("/this_file_does_not_exist.html");
51 |
52 | // Assert
53 | response.StatusCode.Should().Be((HttpStatusCode)999);
54 | }
55 |
56 | ///
57 | /// Serve the uncompressed file if no compressed version exist
58 | ///
59 | ///
60 | [Fact]
61 | public async Task Uncompressed()
62 | {
63 | // Arrange
64 | var builder = new WebHostBuilder()
65 | .ConfigureServices(sp =>
66 | {
67 | sp.AddCompressedStaticFiles();
68 | })
69 | .Configure(app =>
70 | {
71 | app.UseCompressedStaticFiles();
72 | app.Use(next =>
73 | {
74 | return async context =>
75 | {
76 | // this test should never call the next middleware
77 | // set status code to 999 to detect a test failure
78 | context.Response.StatusCode = 999;
79 | };
80 | });
81 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
82 | var server = new TestServer(builder);
83 |
84 | // Act
85 | var response = await server.CreateClient().GetAsync("/i_exist_only_uncompressed.html");
86 | var content = await response.Content.ReadAsStringAsync();
87 |
88 | // Assert
89 | response.StatusCode.Should().Be(HttpStatusCode.OK);
90 | content.Should().Be("uncompressed");
91 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
92 | contentTypeValues.Single().Should().Be("text/html");
93 | }
94 |
95 | ///
96 | /// Serve the compressed file if it exists and the browser supports it testing with a browser that supports both br and gzip
97 | ///
98 | ///
99 | [Fact]
100 | public async Task SupportsBrAndGZip()
101 | {
102 | // Arrange
103 | var builder = new WebHostBuilder()
104 | .ConfigureServices(sp =>
105 | {
106 | sp.AddCompressedStaticFiles();
107 | })
108 | .Configure(app =>
109 | {
110 | app.UseCompressedStaticFiles();
111 | app.Use(next =>
112 | {
113 | return async context =>
114 | {
115 | // this test should never call the next middleware
116 | // set status code to 999 to detect a test failure
117 | context.Response.StatusCode = 999;
118 | };
119 | });
120 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
121 | var server = new TestServer(builder);
122 |
123 | // Act
124 | var client = server.CreateClient();
125 | client.DefaultRequestHeaders.Add("Accept-Encoding", "br, gzip");
126 | var response = await client.GetAsync("/i_also_exist_compressed.html");
127 | var content = await response.Content.ReadAsStringAsync();
128 |
129 | // Assert
130 | response.StatusCode.Should().Be(HttpStatusCode.OK);
131 | content.Should().Be("br");
132 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
133 | contentTypeValues.Single().Should().Be("text/html");
134 | }
135 |
136 | ///
137 | /// Serve the compressed file if it exists and the browser supports it testing with a browser that only supports gzip
138 | ///
139 | ///
140 | [Fact]
141 | public async Task SupportsGzip()
142 | {
143 | // Arrange
144 | var builder = new WebHostBuilder()
145 | .ConfigureServices(sp =>
146 | {
147 | sp.AddCompressedStaticFiles();
148 | })
149 | .Configure(app =>
150 | {
151 | app.UseCompressedStaticFiles();
152 | app.Use(next =>
153 | {
154 | return async context =>
155 | {
156 | // this test should never call the next middleware
157 | // set status code to 999 to detect a test failure
158 | context.Response.StatusCode = 999;
159 | };
160 | });
161 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
162 | var server = new TestServer(builder);
163 |
164 | // Act
165 | var client = server.CreateClient();
166 | client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");
167 | var response = await client.GetAsync("/i_also_exist_compressed.html");
168 | var content = await response.Content.ReadAsStringAsync();
169 |
170 | // Assert
171 | response.StatusCode.Should().Be(HttpStatusCode.OK);
172 | content.Should().Be("gzip");
173 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
174 | contentTypeValues.Single().Should().Be("text/html");
175 | }
176 |
177 | ///
178 | /// Should send the uncompressed file if its smaller than the original
179 | ///
180 | ///
181 | [Fact]
182 | public async Task UncompressedSmaller()
183 | {
184 | // Arrange
185 | var builder = new WebHostBuilder()
186 | .ConfigureServices(sp =>
187 | {
188 | sp.AddCompressedStaticFiles();
189 | })
190 | .Configure(app =>
191 | {
192 | app.UseCompressedStaticFiles();
193 | app.Use(next =>
194 | {
195 | return async context =>
196 | {
197 | // this test should never call the next middleware
198 | // set status code to 999 to detect a test failure
199 | context.Response.StatusCode = 999;
200 | };
201 | });
202 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
203 | var server = new TestServer(builder);
204 |
205 | // Act
206 | var client = server.CreateClient();
207 | client.DefaultRequestHeaders.Add("Accept-Encoding", "br");
208 | var response = await client.GetAsync("/i_am_smaller_in_uncompressed.html");
209 | var content = await response.Content.ReadAsStringAsync();
210 |
211 | // Assert
212 | response.StatusCode.Should().Be(HttpStatusCode.OK);
213 | content.Should().Be("uncompressed");
214 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
215 | contentTypeValues.Single().Should().Be("text/html");
216 | }
217 |
218 | ///
219 | /// Use the FileProvider from options.
220 | ///
221 | [Fact]
222 | public async Task UseCustomFileProvider()
223 | {
224 | // Arrange
225 | var fileInfo = Substitute.For();
226 | fileInfo.Exists.Returns(true);
227 | fileInfo.IsDirectory.Returns(false);
228 | fileInfo.Length.Returns(12);
229 | fileInfo.LastModified.Returns(new DateTimeOffset(2018, 12, 16, 13, 36, 0, new TimeSpan()));
230 | fileInfo.CreateReadStream().Returns(new MemoryStream(Encoding.UTF8.GetBytes("fileprovider")));
231 |
232 | var mockFileProvider = Substitute.For();
233 | mockFileProvider.GetFileInfo("/i_only_exist_in_mociFileProvider.html").Returns(fileInfo);
234 |
235 | var staticFileOptions = new StaticFileOptions() { FileProvider = mockFileProvider };
236 |
237 | var builder = new WebHostBuilder()
238 | .ConfigureServices(sp =>
239 | {
240 | sp.AddCompressedStaticFiles();
241 | })
242 | .Configure(app =>
243 | {
244 | app.UseCompressedStaticFiles(staticFileOptions);
245 | app.Use(next =>
246 | {
247 | return async context =>
248 | {
249 | // this test should never call the next middleware
250 | // set status code to 999 to detect a test failure
251 | context.Response.StatusCode = 999;
252 | };
253 | });
254 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
255 | var server = new TestServer(builder);
256 |
257 | // Act
258 | var client = server.CreateClient();
259 | client.DefaultRequestHeaders.Add("Accept-Encoding", "br");
260 | var response = await client.GetAsync("/i_only_exist_in_mociFileProvider.html");
261 | var content = await response.Content.ReadAsStringAsync();
262 |
263 | // Assert
264 | response.StatusCode.Should().Be(HttpStatusCode.OK);
265 | content.Should().Be("fileprovider");
266 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
267 | contentTypeValues.Single().Should().Be("text/html");
268 | }
269 |
270 | [Fact]
271 | public async Task HandleRequestPath()
272 | {
273 | // Arrange
274 | var builder = new WebHostBuilder()
275 | .ConfigureServices(sp =>
276 | {
277 | sp.AddCompressedStaticFiles();
278 | })
279 | .Configure(app =>
280 | {
281 | app.UseCompressedStaticFiles(new StaticFileOptions
282 | {
283 | RequestPath = "/assets"
284 | });
285 | app.Use(next =>
286 | {
287 | return async context =>
288 | {
289 | // this test should never call the next middleware
290 | // set status code to 999 to detect a test failure
291 | context.Response.StatusCode = 999;
292 | };
293 | });
294 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
295 | var server = new TestServer(builder);
296 |
297 | // Act
298 | var client = server.CreateClient();
299 | client.DefaultRequestHeaders.Add("Accept-Encoding", "br, gzip");
300 | var response = await client.GetAsync("/assets/i_also_exist_compressed.html");
301 | var content = await response.Content.ReadAsStringAsync();
302 |
303 | // Assert
304 | response.StatusCode.Should().Be(HttpStatusCode.OK);
305 | content.Should().Be("br");
306 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
307 | contentTypeValues.Single().Should().Be("text/html");
308 | }
309 |
310 | ///
311 | /// Should not send precompressed content if it has been disabled.
312 | ///
313 | ///
314 | [Fact]
315 | public async Task Disabled()
316 | {
317 | // Arrange
318 | var builder = new WebHostBuilder()
319 | .ConfigureServices(sp =>
320 | {
321 | sp.AddCompressedStaticFiles(options => options.EnablePrecompressedFiles = false);
322 | })
323 | .Configure(app =>
324 | {
325 | app.UseCompressedStaticFiles();
326 | app.Use(next =>
327 | {
328 | return async context =>
329 | {
330 | // this test should never call the next middleware
331 | // set status code to 999 to detect a test failure
332 | context.Response.StatusCode = 999;
333 | };
334 | });
335 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
336 | var server = new TestServer(builder);
337 |
338 | // Act
339 | var client = server.CreateClient();
340 | client.DefaultRequestHeaders.Add("Accept-Encoding", "br");
341 | var response = await client.GetAsync("/i_also_exist_compressed.html");
342 | var content = await response.Content.ReadAsStringAsync();
343 |
344 | // Assert
345 | response.StatusCode.Should().Be(HttpStatusCode.OK);
346 | content.Should().Be("uncompressed");
347 | }
348 |
349 | ///
350 | /// Files with spaces in the file name should be supported
351 | ///
352 | ///
353 | [Fact]
354 | public async Task SupportsSpaces()
355 | {
356 | // Arrange
357 | var builder = new WebHostBuilder()
358 | .ConfigureServices(sp => {
359 | sp.AddCompressedStaticFiles();
360 | })
361 | .Configure(app => {
362 | app.UseCompressedStaticFiles();
363 | app.Use(next => {
364 | return async context => {
365 | // this test should never call the next middleware
366 | // set status code to 999 to detect a test failure
367 | context.Response.StatusCode = 999;
368 | };
369 | });
370 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
371 | var server = new TestServer(builder);
372 |
373 | // Act
374 | var client = server.CreateClient();
375 | client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");
376 | var response = await client.GetAsync("/with spaces.html");
377 | var content = await response.Content.ReadAsStringAsync();
378 |
379 | // Assert
380 | response.StatusCode.Should().Be(HttpStatusCode.OK);
381 | content.Should().Be("gzip");
382 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
383 | contentTypeValues.Single().Should().Be("text/html");
384 | }
385 |
386 | ///
387 | /// Files with spaces in the file name should be supported
388 | ///
389 | ///
390 | [Fact]
391 | public async Task SupportsSpaces_Encoded() {
392 | // Arrange
393 | var builder = new WebHostBuilder()
394 | .ConfigureServices(sp => {
395 | sp.AddCompressedStaticFiles();
396 | })
397 | .Configure(app => {
398 | app.UseCompressedStaticFiles();
399 | app.Use(next => {
400 | return async context => {
401 | // this test should never call the next middleware
402 | // set status code to 999 to detect a test failure
403 | context.Response.StatusCode = 999;
404 | };
405 | });
406 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
407 | var server = new TestServer(builder);
408 |
409 | // Act
410 | var client = server.CreateClient();
411 | client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");
412 | var response = await client.GetAsync("/with%20spaces.html");
413 | var content = await response.Content.ReadAsStringAsync();
414 |
415 | // Assert
416 | response.StatusCode.Should().Be(HttpStatusCode.OK);
417 | content.Should().Be("gzip");
418 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
419 | contentTypeValues.Single().Should().Be("text/html");
420 | }
421 | }
422 | }
423 |
424 |
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/CompressedStaticFiles.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0;
5 | true
6 | false
7 |
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 |
18 |
19 |
20 | all
21 | runtime; build; native; contentfiles; analyzers; buildtransitive
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | PreserveNewest
32 |
33 |
34 | PreserveNewest
35 |
36 |
37 | PreserveNewest
38 |
39 |
40 | PreserveNewest
41 |
42 |
43 | PreserveNewest
44 |
45 |
46 | PreserveNewest
47 |
48 |
49 | PreserveNewest
50 |
51 |
52 | PreserveNewest
53 |
54 |
55 | PreserveNewest
56 |
57 |
58 | PreserveNewest
59 |
60 |
61 | PreserveNewest
62 |
63 |
64 | PreserveNewest
65 |
66 |
67 | PreserveNewest
68 |
69 |
70 | PreserveNewest
71 |
72 |
73 | PreserveNewest
74 |
75 |
76 | PreserveNewest
77 |
78 |
79 | PreserveNewest
80 |
81 |
82 | PreserveNewest
83 |
84 |
85 | PreserveNewest
86 |
87 |
88 | PreserveNewest
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/ImageCompressedStaticFileMiddlewareTests.cs:
--------------------------------------------------------------------------------
1 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
2 |
3 | using FluentAssertions;
4 | using Microsoft.AspNetCore.Hosting;
5 | using Microsoft.AspNetCore.TestHost;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.IO;
9 | using System.Linq;
10 | using System.Net;
11 | using System.Threading.Tasks;
12 | using Xunit;
13 |
14 | namespace CompressedStaticFiles.Tests
15 | {
16 | public class ImageCompressedStaticFileMiddlewareTests
17 | {
18 | [Fact]
19 | public async Task GetSmallest()
20 | {
21 | // Arrange
22 | var builder = new WebHostBuilder()
23 | .ConfigureServices(sp =>
24 | {
25 | sp.AddCompressedStaticFiles();
26 | })
27 | .Configure(app =>
28 | {
29 | app.UseCompressedStaticFiles();
30 | app.Use(next =>
31 | {
32 | return async context =>
33 | {
34 | // this test should never call the next middleware
35 | // set status code to 999 to detect a test failure
36 | context.Response.StatusCode = 999;
37 | };
38 | });
39 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
40 | var server = new TestServer(builder);
41 |
42 | // Act
43 | var client = server.CreateClient();
44 | client.DefaultRequestHeaders.Add("Accept", "image/avif,image/webp");
45 | var response = await client.GetAsync("/IMG_6067.jpg");
46 | var content = await response.Content.ReadAsStringAsync();
47 |
48 | // Assert
49 | response.StatusCode.Should().Be(HttpStatusCode.OK);
50 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
51 | contentTypeValues.Single().Should().Be("image/avif");
52 | }
53 |
54 | [Fact]
55 | public async Task FavIcon()
56 | {
57 | // Arrange
58 | var builder = new WebHostBuilder()
59 | .ConfigureServices(sp =>
60 | {
61 | sp.AddCompressedStaticFiles();
62 | })
63 | .Configure(app =>
64 | {
65 | app.UseCompressedStaticFiles();
66 | app.Use(next =>
67 | {
68 | return async context =>
69 | {
70 | // this test should never call the next middleware
71 | // set status code to 999 to detect a test failure
72 | context.Response.StatusCode = 999;
73 | };
74 | });
75 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
76 | var server = new TestServer(builder);
77 |
78 | // Act
79 | var client = server.CreateClient();
80 | client.DefaultRequestHeaders.Add("Accept-Encoding", "br, gzip");
81 | client.DefaultRequestHeaders.Add("Accept", "image/png,image/avif,image/webp");
82 | var response = await client.GetAsync("/favicon.ico");
83 | var content = await response.Content.ReadAsStringAsync();
84 |
85 | // Assert
86 | response.StatusCode.Should().Be(HttpStatusCode.OK);
87 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
88 | contentTypeValues.Single().Should().Be("image/webp");
89 | }
90 |
91 | [Fact]
92 | public async Task GetSecondSmallest()
93 | {
94 | // Arrange
95 | var builder = new WebHostBuilder()
96 | .ConfigureServices(sp =>
97 | {
98 | sp.AddCompressedStaticFiles();
99 | })
100 | .Configure(app =>
101 | {
102 | app.UseCompressedStaticFiles();
103 | app.Use(next =>
104 | {
105 | return async context =>
106 | {
107 | // this test should never call the next middleware
108 | // set status code to 999 to detect a test failure
109 | context.Response.StatusCode = 999;
110 | };
111 | });
112 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
113 | var server = new TestServer(builder);
114 |
115 | // Act
116 | var client = server.CreateClient();
117 | client.DefaultRequestHeaders.Add("Accept", "image/webp");
118 | var response = await client.GetAsync("/IMG_6067.jpg");
119 | var content = await response.Content.ReadAsStringAsync();
120 |
121 | // Assert
122 | response.StatusCode.Should().Be(HttpStatusCode.OK);
123 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
124 | contentTypeValues.Single().Should().Be("image/webp");
125 | }
126 |
127 | [Fact]
128 | public async Task ShouldNotHaveAcceptEncoding()
129 | {
130 | // Arrange
131 | var builder = new WebHostBuilder()
132 | .ConfigureServices(sp =>
133 | {
134 | sp.AddCompressedStaticFiles();
135 | })
136 | .Configure(app =>
137 | {
138 | app.UseCompressedStaticFiles();
139 | app.Use(next =>
140 | {
141 | return async context =>
142 | {
143 | // this test should never call the next middleware
144 | // set status code to 999 to detect a test failure
145 | context.Response.StatusCode = 999;
146 | };
147 | });
148 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
149 | var server = new TestServer(builder);
150 |
151 | // Act
152 | var client = server.CreateClient();
153 | client.DefaultRequestHeaders.Add("Accept", "image/webp");
154 | var response = await client.GetAsync("/IMG_6067.jpg");
155 | var content = await response.Content.ReadAsStringAsync();
156 |
157 | // Assert
158 | response.StatusCode.Should().Be(HttpStatusCode.OK);
159 | response.Content.Headers.Contains("Content-Encoding").Should().BeFalse();
160 | }
161 |
162 | [Fact]
163 | public async Task GetWithoutSupport()
164 | {
165 | // Arrange
166 | var builder = new WebHostBuilder()
167 | .ConfigureServices(sp =>
168 | {
169 | sp.AddCompressedStaticFiles();
170 | })
171 | .Configure(app =>
172 | {
173 | app.UseCompressedStaticFiles();
174 | app.Use(next =>
175 | {
176 | return async context =>
177 | {
178 | // this test should never call the next middleware
179 | // set status code to 999 to detect a test failure
180 | context.Response.StatusCode = 999;
181 | };
182 | });
183 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
184 | var server = new TestServer(builder);
185 |
186 | // Act
187 | var client = server.CreateClient();
188 | var response = await client.GetAsync("/IMG_6067.jpg");
189 | var content = await response.Content.ReadAsStringAsync();
190 |
191 | // Assert
192 | response.StatusCode.Should().Be(HttpStatusCode.OK);
193 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
194 | contentTypeValues.Single().Should().Be("image/jpeg");
195 | }
196 |
197 | [Fact]
198 | public async Task Disabled()
199 | {
200 | // Arrange
201 | var builder = new WebHostBuilder()
202 | .ConfigureServices(sp =>
203 | {
204 | sp.AddCompressedStaticFiles(options => options.EnableImageSubstitution = false); ;
205 | })
206 | .Configure(app =>
207 | {
208 | app.UseCompressedStaticFiles();
209 | app.Use(next =>
210 | {
211 | return async context =>
212 | {
213 | // this test should never call the next middleware
214 | // set status code to 999 to detect a test failure
215 | context.Response.StatusCode = 999;
216 | };
217 | });
218 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
219 | var server = new TestServer(builder);
220 |
221 | // Act
222 | var client = server.CreateClient();
223 | client.DefaultRequestHeaders.Add("Accept", "image/png,image/avif,image/webp");
224 | var response = await client.GetAsync("/IMG_6067.jpg");
225 | var content = await response.Content.ReadAsStringAsync();
226 |
227 | // Assert
228 | response.StatusCode.Should().Be(HttpStatusCode.OK);
229 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
230 | contentTypeValues.Single().Should().Be("image/jpeg");
231 | }
232 |
233 | [Fact]
234 | public async Task PrioritizeSmallest()
235 | {
236 | // Arrange
237 | var builder = new WebHostBuilder()
238 | .ConfigureServices(sp =>
239 | {
240 | sp.AddCompressedStaticFiles(options => options.RemoveImageSubstitutionCostRatio());
241 | })
242 | .Configure(app =>
243 | {
244 | app.UseCompressedStaticFiles();
245 | app.Use(next =>
246 | {
247 | return async context =>
248 | {
249 | // this test should never call the next middleware
250 | // set status code to 999 to detect a test failure
251 | context.Response.StatusCode = 999;
252 | };
253 | });
254 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
255 | var server = new TestServer(builder);
256 |
257 | // Act
258 | var client = server.CreateClient();
259 | client.DefaultRequestHeaders.Add("Accept", "image/png,image/avif,image/webp");
260 | var response = await client.GetAsync("/highquality.jpg");
261 | var content = await response.Content.ReadAsStringAsync();
262 |
263 | // Assert
264 | response.StatusCode.Should().Be(HttpStatusCode.OK);
265 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
266 | contentTypeValues.Single().Should().Be("image/jpeg");
267 | }
268 |
269 | [Fact]
270 | public async Task PrioritizeQualityAVIF()
271 | {
272 | // Arrange
273 | var builder = new WebHostBuilder()
274 | .ConfigureServices(sp =>
275 | {
276 | sp.AddCompressedStaticFiles();
277 | })
278 | .Configure(app =>
279 | {
280 | app.UseCompressedStaticFiles();
281 | app.Use(next =>
282 | {
283 | return async context =>
284 | {
285 | // this test should never call the next middleware
286 | // set status code to 999 to detect a test failure
287 | context.Response.StatusCode = 999;
288 | };
289 | });
290 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
291 | var server = new TestServer(builder);
292 |
293 | // Act
294 | var client = server.CreateClient();
295 | client.DefaultRequestHeaders.Add("Accept", "image/png,image/avif,image/webp");
296 | var response = await client.GetAsync("/highquality.jpg");
297 | var content = await response.Content.ReadAsStringAsync();
298 |
299 | // Assert
300 | response.StatusCode.Should().Be(HttpStatusCode.OK);
301 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
302 | contentTypeValues.Single().Should().Be("image/avif");
303 | }
304 |
305 | [Fact]
306 | public async Task PrioritizeQualityWEBP()
307 | {
308 | // Arrange
309 | var builder = new WebHostBuilder()
310 | .ConfigureServices(sp =>
311 | {
312 | sp.AddCompressedStaticFiles();
313 | })
314 | .Configure(app =>
315 | {
316 | app.UseCompressedStaticFiles();
317 | app.Use(next =>
318 | {
319 | return async context =>
320 | {
321 | // this test should never call the next middleware
322 | // set status code to 999 to detect a test failure
323 | context.Response.StatusCode = 999;
324 | };
325 | });
326 | }).UseWebRoot(Path.Combine(Environment.CurrentDirectory, "wwwroot"));
327 | var server = new TestServer(builder);
328 |
329 | // Act
330 | var client = server.CreateClient();
331 | client.DefaultRequestHeaders.Add("Accept", "image/png,image/webp");
332 | var response = await client.GetAsync("/highquality.jpg");
333 | var content = await response.Content.ReadAsStringAsync();
334 |
335 | // Assert
336 | response.StatusCode.Should().Be(HttpStatusCode.OK);
337 | response.Content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues);
338 | contentTypeValues.Single().Should().Be("image/webp");
339 | }
340 | }
341 | }
342 |
343 |
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/IMG_6067.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/IMG_6067.avif
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/IMG_6067.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/IMG_6067.jpg
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/IMG_6067.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/IMG_6067.webp
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/favicon.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/favicon.avif
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/favicon.ico.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/favicon.ico.br
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/favicon.ico.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/favicon.ico.gz
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/favicon.png
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/favicon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/favicon.webp
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/generate_highquality.bat:
--------------------------------------------------------------------------------
1 | cwebp -size 86018 -m 6 highquality.png -o highquality.webp
2 | avifenc --min 5 --max 5 -s 0 highquality.png -o highquality.avif
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/highquality.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/highquality.avif
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/highquality.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/highquality.jpg
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/highquality.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/highquality.png
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/highquality.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/highquality.webp
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/i_also_exist_compressed.html:
--------------------------------------------------------------------------------
1 | uncompressed
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/i_also_exist_compressed.html.br:
--------------------------------------------------------------------------------
1 | br
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/i_also_exist_compressed.html.gz:
--------------------------------------------------------------------------------
1 | gzip
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/i_am_smaller_in_uncompressed.html:
--------------------------------------------------------------------------------
1 | uncompressed
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/i_am_smaller_in_uncompressed.html.br:
--------------------------------------------------------------------------------
1 | br is longer than uncompressed in this case
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/i_exist_only_uncompressed.html:
--------------------------------------------------------------------------------
1 | uncompressed
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/icon.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/icon.avif
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/icon.png
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles.Tests/wwwroot/icon.webp
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/with spaces.html:
--------------------------------------------------------------------------------
1 | uncompressed
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/with spaces.html.br:
--------------------------------------------------------------------------------
1 | br
--------------------------------------------------------------------------------
/CompressedStaticFiles.Tests/wwwroot/with spaces.html.gz:
--------------------------------------------------------------------------------
1 | gzip
--------------------------------------------------------------------------------
/CompressedStaticFiles.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29806.167
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{52861BD1-852A-45A9-9B32-2F042501228A}"
7 | ProjectSection(SolutionItems) = preProject
8 | .gitattributes = .gitattributes
9 | .gitignore = .gitignore
10 | global.json = global.json
11 | LICENSE = LICENSE
12 | README.md = README.md
13 | EndProjectSection
14 | EndProject
15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompressedStaticFiles.Tests", "CompressedStaticFiles.Tests\CompressedStaticFiles.Tests.csproj", "{3D5A45B2-A15B-49B2-BB4F-47FDC1B5A5ED}"
16 | EndProject
17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompressedStaticFiles", "CompressedStaticFiles\CompressedStaticFiles.csproj", "{0839E928-AD6E-493A-A7C0-311B8C8905E4}"
18 | EndProject
19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example", "Example\Example.csproj", "{560BE25F-638D-4B3C-BE63-D427D6C516C5}"
20 | EndProject
21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{ADB0F1E4-D83F-4E58-80DD-57AD26C22DD5}"
22 | EndProject
23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{207C684E-30A0-4D6E-821E-310BE08EBF28}"
24 | ProjectSection(SolutionItems) = preProject
25 | .github\workflows\test.yml = .github\workflows\test.yml
26 | EndProjectSection
27 | EndProject
28 | Global
29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
30 | Debug|Any CPU = Debug|Any CPU
31 | Release|Any CPU = Release|Any CPU
32 | EndGlobalSection
33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
34 | {3D5A45B2-A15B-49B2-BB4F-47FDC1B5A5ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {3D5A45B2-A15B-49B2-BB4F-47FDC1B5A5ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {3D5A45B2-A15B-49B2-BB4F-47FDC1B5A5ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {3D5A45B2-A15B-49B2-BB4F-47FDC1B5A5ED}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {0839E928-AD6E-493A-A7C0-311B8C8905E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {0839E928-AD6E-493A-A7C0-311B8C8905E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {0839E928-AD6E-493A-A7C0-311B8C8905E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {0839E928-AD6E-493A-A7C0-311B8C8905E4}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {560BE25F-638D-4B3C-BE63-D427D6C516C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43 | {560BE25F-638D-4B3C-BE63-D427D6C516C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
44 | {560BE25F-638D-4B3C-BE63-D427D6C516C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
45 | {560BE25F-638D-4B3C-BE63-D427D6C516C5}.Release|Any CPU.Build.0 = Release|Any CPU
46 | EndGlobalSection
47 | GlobalSection(SolutionProperties) = preSolution
48 | HideSolutionNode = FALSE
49 | EndGlobalSection
50 | GlobalSection(NestedProjects) = preSolution
51 | {ADB0F1E4-D83F-4E58-80DD-57AD26C22DD5} = {52861BD1-852A-45A9-9B32-2F042501228A}
52 | {207C684E-30A0-4D6E-821E-310BE08EBF28} = {ADB0F1E4-D83F-4E58-80DD-57AD26C22DD5}
53 | EndGlobalSection
54 | GlobalSection(ExtensibilityGlobals) = postSolution
55 | SolutionGuid = {D8781CF2-14D0-4C67-A2D7-C8AD7BBCF2CE}
56 | EndGlobalSection
57 | EndGlobal
58 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/AlternativeImageFile.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.StaticFiles;
3 | using Microsoft.Extensions.FileProviders;
4 | using Microsoft.Extensions.Logging;
5 | using System.IO;
6 |
7 | namespace CompressedStaticFiles
8 | {
9 | public class AlternativeImageFile : IFileAlternative
10 | {
11 | private readonly ILogger logger;
12 | private readonly IFileInfo originalFile;
13 | private readonly IFileInfo alternativeFile;
14 | private readonly float costRatio;
15 |
16 | public AlternativeImageFile(ILogger logger, IFileInfo originalFile, IFileInfo alternativeFile, float costRatio)
17 | {
18 | this.logger = logger;
19 | this.originalFile = originalFile;
20 | this.alternativeFile = alternativeFile;
21 | this.costRatio = costRatio;
22 | }
23 |
24 | public long Size => alternativeFile.Length;
25 |
26 | public float Cost => Size * costRatio;
27 |
28 | public void Apply(HttpContext context)
29 | {
30 | var path = context.Request.Path.Value;
31 | //Change file extension!
32 | var pathAndFilenameWithoutExtension = path.Substring(0, path.LastIndexOf('.'));
33 | var matchedPath = pathAndFilenameWithoutExtension + Path.GetExtension(alternativeFile.Name);
34 | logger.LogFileServed(context.Request.Path.Value, matchedPath, originalFile.Length, alternativeFile.Length);
35 | //Redirect the static file system to the alternative file
36 | context.Request.Path = new PathString(matchedPath);
37 | //Ensure that a caching proxy knows that it should cache based on the Accept header.
38 | context.Response.Headers.Add("Vary", "Accept");
39 | }
40 |
41 | public void Prepare(IContentTypeProvider contentTypeProvider, StaticFileResponseContext staticFileResponseContext)
42 | {
43 | }
44 |
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/AlternativeImageFileProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.StaticFiles;
3 | using Microsoft.Extensions.FileProviders;
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Options;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.IO;
9 | using System.Linq;
10 |
11 | namespace CompressedStaticFiles
12 | {
13 | public class AlternativeImageFileProvider : IAlternativeFileProvider
14 | {
15 |
16 | private static Dictionary imageFormats =
17 | new Dictionary(StringComparer.OrdinalIgnoreCase)
18 | {
19 | { "image/avif", new [] { ".avif" } },
20 | { "image/webp", new [] { ".webp" } },
21 | { "image/jpeg", new [] { ".jpg", ".jpeg", ".jfif", ".pjpeg", ".pjp" } },
22 | { "image/png", new [] { ".png" } },
23 | { "image/bmp", new [] { ".bmp" } },
24 | { "image/apng", new [] { ".apng" } },
25 | { "image/gif", new [] { ".gif" } },
26 | { "image/x-icon", new [] { ".ico", ".cur" } },
27 | { "image/tiff", new [] { ".tif", ".tiff" } }
28 | };
29 | private readonly ILogger logger;
30 | private readonly IOptions options;
31 |
32 | public AlternativeImageFileProvider(ILogger logger, IOptions options)
33 | {
34 | this.logger = logger;
35 | this.options = options;
36 | }
37 |
38 | public void Initialize(FileExtensionContentTypeProvider fileExtensionContentTypeProvider)
39 | {
40 | //Ensure that all image mime types are known!
41 | foreach (var mimeType in imageFormats.Keys)
42 | {
43 | foreach (var fileExtension in imageFormats[mimeType])
44 | {
45 | if (!fileExtensionContentTypeProvider.Mappings.ContainsKey(fileExtension))
46 | {
47 | fileExtensionContentTypeProvider.Mappings.Add(fileExtension, mimeType);
48 | }
49 | }
50 | }
51 | }
52 |
53 | private float GetCostRatioForFileExtension(string fileExtension)
54 | {
55 | foreach (var mimeType in imageFormats.Keys)
56 | {
57 | if (imageFormats[mimeType].Contains(fileExtension))
58 | {
59 | if (options.Value.ImageSubstitutionCostRatio.TryGetValue(mimeType, out var cost))
60 | {
61 | return cost;
62 | }
63 | return 1;
64 | }
65 | }
66 | return 1;
67 | }
68 |
69 |
70 | private float GetCostRatioForPath(string path)
71 | {
72 | var fileExtension = Path.GetExtension(path);
73 | return GetCostRatioForFileExtension(fileExtension);
74 | }
75 |
76 | public IFileAlternative GetAlternative(HttpContext context, IFileProvider fileSystem, IFileInfo originalFile, PathString filePath)
77 | {
78 | if (!options.Value.EnableImageSubstitution)
79 | {
80 | return null;
81 | }
82 |
83 | var matchingFileExtensions = context.Request.Headers.GetCommaSeparatedValues("Accept")
84 | .Where(mimeType => imageFormats.ContainsKey(mimeType))
85 | .SelectMany(mimeType => imageFormats[mimeType]);
86 |
87 | var originalAlternativeImageFile = new AlternativeImageFile(logger, originalFile, originalFile, GetCostRatioForPath(originalFile.PhysicalPath));
88 |
89 | AlternativeImageFile matchedFile = originalAlternativeImageFile;
90 | var path = filePath.Value;
91 | if (!path.Contains('.'))
92 | {
93 | //no file extension, here is no way to check for alternatives
94 | return null;
95 | }
96 | var withoutExtension = path.Substring(0, path.LastIndexOf('.'));
97 | foreach (var fileExtension in matchingFileExtensions)
98 | {
99 | var file = fileSystem.GetFileInfo(withoutExtension + fileExtension);
100 | if (file.Exists)
101 | {
102 | var alternativeFile = new AlternativeImageFile(logger, originalFile, file, GetCostRatioForFileExtension(fileExtension));
103 | if (matchedFile.Cost > alternativeFile.Cost)
104 | {
105 | matchedFile = alternativeFile;
106 | }
107 | }
108 | }
109 |
110 | if (matchedFile != originalAlternativeImageFile)
111 | {
112 | return matchedFile;
113 | }
114 |
115 | return null;
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/CompressedAlternativeFile.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.StaticFiles;
3 | using Microsoft.Extensions.FileProviders;
4 | using Microsoft.Extensions.Logging;
5 | using System;
6 | using System.IO;
7 |
8 | namespace CompressedStaticFiles
9 | {
10 | public class CompressedAlternativeFile : IFileAlternative
11 | {
12 | private readonly ILogger logger;
13 | private readonly IFileInfo originalFile;
14 | private readonly IFileInfo alternativeFile;
15 |
16 | public CompressedAlternativeFile(ILogger logger, IFileInfo originalFile, IFileInfo alternativeFile)
17 | {
18 | this.logger = logger;
19 | this.originalFile = originalFile;
20 | this.alternativeFile = alternativeFile;
21 | }
22 |
23 | public long Size => alternativeFile.Length;
24 |
25 | public float Cost => Size;
26 |
27 | public void Apply(HttpContext context)
28 | {
29 | var matchedPath = context.Request.Path.Value + Path.GetExtension(alternativeFile.Name);
30 | logger.LogFileServed(context.Request.Path.Value, matchedPath, originalFile.Length, alternativeFile.Length);
31 | context.Request.Path = new PathString(matchedPath);
32 | }
33 |
34 | public void Prepare(IContentTypeProvider contentTypeProvider, StaticFileResponseContext staticFileResponseContext)
35 | {
36 | foreach (var compressionType in CompressedAlternativeFileProvider.CompressionTypes.Keys)
37 | {
38 | var fileExtension = CompressedAlternativeFileProvider.CompressionTypes[compressionType];
39 | if (staticFileResponseContext.File.Name.EndsWith(fileExtension, StringComparison.OrdinalIgnoreCase))
40 | {
41 | // we need to restore the original content type, otherwise it would be based on the compression type
42 | // (for example "application/brotli" instead of "text/html")
43 | if (contentTypeProvider.TryGetContentType(staticFileResponseContext.File.PhysicalPath.Remove(
44 | staticFileResponseContext.File.PhysicalPath.Length - fileExtension.Length, fileExtension.Length), out var contentType))
45 | staticFileResponseContext.Context.Response.ContentType = contentType;
46 | staticFileResponseContext.Context.Response.Headers.Add("Content-Encoding", new[] { compressionType });
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/CompressedAlternativeFileProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.StaticFiles;
3 | using Microsoft.Extensions.FileProviders;
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Options;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.IO;
9 | using System.Linq;
10 |
11 | namespace CompressedStaticFiles
12 | {
13 | public class CompressedAlternativeFileProvider : IAlternativeFileProvider
14 | {
15 | public static Dictionary CompressionTypes =
16 | new Dictionary()
17 | {
18 | { "gzip", ".gz" },
19 | { "br", ".br" },
20 | { "zstd", ".zst" }
21 | };
22 |
23 | private readonly ILogger logger;
24 | private readonly IOptions options;
25 |
26 | public CompressedAlternativeFileProvider(ILogger logger, IOptions options)
27 | {
28 | this.logger = logger;
29 | this.options = options;
30 | }
31 |
32 | public void Initialize(FileExtensionContentTypeProvider fileExtensionContentTypeProvider)
33 | {
34 | // the StaticFileProvider would not serve the file if it does not know the content-type
35 | if (!fileExtensionContentTypeProvider.Mappings.ContainsKey(".br")) {
36 | fileExtensionContentTypeProvider.Mappings[".br"] = "application/brotli";
37 | }
38 | if (!fileExtensionContentTypeProvider.Mappings.ContainsKey(".zst"))
39 | {
40 | fileExtensionContentTypeProvider.Mappings[".zst"] = "application/zstd";
41 | }
42 | }
43 |
44 | ///
45 | /// Find the encodings that are supported by the browser and by this middleware
46 | ///
47 | private static IEnumerable GetSupportedEncodings(HttpContext context)
48 | {
49 | var browserSupportedCompressionTypes = context.Request.Headers.GetCommaSeparatedValues("Accept-Encoding");
50 | var validCompressionTypes = CompressionTypes.Keys.Intersect(browserSupportedCompressionTypes, StringComparer.OrdinalIgnoreCase);
51 | return validCompressionTypes;
52 | }
53 |
54 | public IFileAlternative GetAlternative(HttpContext context, IFileProvider fileSystem, IFileInfo originalFile, PathString filePath)
55 | {
56 | if (!options.Value.EnablePrecompressedFiles)
57 | {
58 | return null;
59 | }
60 | var supportedEncodings = GetSupportedEncodings(context);
61 | IFileInfo matchedFile = originalFile;
62 | foreach (var compressionType in supportedEncodings)
63 | {
64 | var fileExtension = CompressionTypes[compressionType];
65 | var file = fileSystem.GetFileInfo(filePath.Value + fileExtension);
66 | if (file.Exists && file.Length < matchedFile.Length)
67 | {
68 | matchedFile = file;
69 | }
70 | }
71 |
72 | if (matchedFile != originalFile)
73 | {
74 | // a compressed version exists and is smaller, change the path to serve the compressed file
75 | var matchedPath = context.Request.Path.Value + Path.GetExtension(matchedFile.Name);
76 | return new CompressedAlternativeFile(logger, originalFile, matchedFile);
77 | }
78 | return null;
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/CompressedStaticFileExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Options;
5 | using System;
6 | using System.Collections.Generic;
7 |
8 | namespace CompressedStaticFiles
9 | {
10 | public static class CompressedStaticFileExtensions
11 | {
12 | public static CompressedStaticFileOptions RemoveImageSubstitutionCostRatio(this CompressedStaticFileOptions compressedStaticFileOptions)
13 | {
14 | compressedStaticFileOptions.ImageSubstitutionCostRatio.Clear();
15 | return compressedStaticFileOptions;
16 | }
17 |
18 | public static IServiceCollection AddCompressedStaticFiles(this IServiceCollection services)
19 | {
20 | services.AddSingleton();
21 | services.AddSingleton();
22 | return services;
23 | }
24 |
25 | public static IServiceCollection AddCompressedStaticFiles(this IServiceCollection services, Action configureOptions)
26 | {
27 | services.Configure(configureOptions);
28 | services.AddSingleton();
29 | services.AddSingleton();
30 | return services;
31 | }
32 |
33 | public static IApplicationBuilder UseCompressedStaticFiles(this IApplicationBuilder app)
34 | {
35 | if (app == null)
36 | {
37 | throw new ArgumentNullException(nameof(app));
38 | }
39 |
40 | return app.UseMiddleware();
41 | }
42 |
43 | public static IApplicationBuilder UseCompressedStaticFiles(this IApplicationBuilder app, StaticFileOptions staticFileOptions)
44 | {
45 | if (app == null)
46 | {
47 | throw new ArgumentNullException(nameof(app));
48 | }
49 |
50 | return app.UseMiddleware(Options.Create(staticFileOptions));
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/CompressedStaticFileMiddleware.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.AspNetCore.Http;
4 | using Microsoft.AspNetCore.Http.Headers;
5 | using Microsoft.AspNetCore.StaticFiles;
6 | using Microsoft.Extensions.FileProviders;
7 | using Microsoft.Extensions.Logging;
8 | using Microsoft.Extensions.Options;
9 | using System;
10 | using System.Collections.Generic;
11 | using System.IO;
12 | using System.Linq;
13 | using System.Threading;
14 | using System.Threading.Tasks;
15 | #if NETSTANDARD2_0
16 | using IHost = Microsoft.AspNetCore.Hosting.IHostingEnvironment;
17 | #else
18 | using IHost = Microsoft.AspNetCore.Hosting.IWebHostEnvironment;
19 | #endif
20 |
21 |
22 | namespace CompressedStaticFiles
23 | {
24 | public class CompressedStaticFileMiddleware
25 | {
26 | private readonly AsyncLocal alternativeFile = new AsyncLocal();
27 | private readonly IOptions _staticFileOptions;
28 | private readonly IEnumerable alternativeFileProviders;
29 | private readonly StaticFileMiddleware _base;
30 | private readonly ILogger logger;
31 |
32 | public CompressedStaticFileMiddleware(
33 | RequestDelegate next,
34 | IHost hostingEnv,
35 | IOptions staticFileOptions, IOptions compressedStaticFileOptions, ILoggerFactory loggerFactory, IEnumerable alternativeFileProviders)
36 | {
37 | if (next == null)
38 | {
39 | throw new ArgumentNullException(nameof(next));
40 | }
41 |
42 | if (loggerFactory == null)
43 | {
44 | throw new ArgumentNullException(nameof(loggerFactory));
45 | }
46 |
47 | if (hostingEnv == null)
48 | {
49 | throw new ArgumentNullException(nameof(hostingEnv));
50 | }
51 | if (!alternativeFileProviders.Any())
52 | {
53 | throw new Exception("No IAlternativeFileProviders where found, did you forget to add AddCompressedStaticFiles() in ConfigureServices?");
54 | }
55 | logger = loggerFactory.CreateLogger();
56 |
57 |
58 | this._staticFileOptions = staticFileOptions ?? throw new ArgumentNullException(nameof(staticFileOptions));
59 | this.alternativeFileProviders = alternativeFileProviders;
60 | InitializeStaticFileOptions(hostingEnv, staticFileOptions);
61 |
62 | _base = new StaticFileMiddleware(next, hostingEnv, staticFileOptions, loggerFactory);
63 | }
64 |
65 | private void InitializeStaticFileOptions(IHost hostingEnv, IOptions staticFileOptions)
66 | {
67 | staticFileOptions.Value.FileProvider = staticFileOptions.Value.FileProvider ?? hostingEnv.WebRootFileProvider;
68 | var contentTypeProvider = staticFileOptions.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
69 | if (contentTypeProvider is FileExtensionContentTypeProvider fileExtensionContentTypeProvider)
70 | {
71 | foreach(var alternativeFileProvider in alternativeFileProviders)
72 | {
73 | alternativeFileProvider.Initialize(fileExtensionContentTypeProvider);
74 | }
75 |
76 | }
77 | staticFileOptions.Value.ContentTypeProvider = contentTypeProvider;
78 |
79 | var originalPrepareResponse = staticFileOptions.Value.OnPrepareResponse;
80 | staticFileOptions.Value.OnPrepareResponse = context =>
81 | {
82 | originalPrepareResponse(context);
83 | var alternativeFile = this.alternativeFile.Value;
84 | if (alternativeFile != null)
85 | {
86 | alternativeFile.Prepare(contentTypeProvider, context);
87 | }
88 |
89 | };
90 | }
91 |
92 | public Task Invoke(HttpContext context)
93 | {
94 | if (context.Request.Path.HasValue)
95 | {
96 | ProcessRequest(context);
97 | }
98 | return _base.Invoke(context);
99 | }
100 |
101 | private void ProcessRequest(HttpContext context)
102 | {
103 | var fileSystem = _staticFileOptions.Value.FileProvider;
104 | if (!context.Request.Path.StartsWithSegments(_staticFileOptions.Value.RequestPath, out var filePath))
105 | {
106 | return;
107 | }
108 |
109 | var originalFile = fileSystem.GetFileInfo(filePath.Value);
110 |
111 | if (!originalFile.Exists || originalFile.IsDirectory)
112 | {
113 | return;
114 | }
115 |
116 | //Find the smallest file from all our alternative file providers
117 | var smallestAlternativeFile = alternativeFileProviders.Select(alternativeFileProvider => alternativeFileProvider.GetAlternative(context, fileSystem, originalFile, filePath))
118 | .Where(af => af != null)
119 | .OrderBy(alternativeFile => alternativeFile?.Cost)
120 | .FirstOrDefault();
121 | if (smallestAlternativeFile != null)
122 | {
123 | smallestAlternativeFile.Apply(context);
124 | alternativeFile.Value = smallestAlternativeFile;
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/CompressedStaticFileOptions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace CompressedStaticFiles
4 | {
5 | public class CompressedStaticFileOptions
6 | {
7 | public bool EnablePrecompressedFiles { get; set; } = true;
8 | public bool EnableImageSubstitution { get; set; } = true;
9 |
10 | ///
11 | /// Used to prioritize image formats that contain higher quality per byte, if only size should be considered remove all entries.
12 | ///
13 | public Dictionary ImageSubstitutionCostRatio { get; set; } = new Dictionary()
14 | {
15 | { "image/bmp", 2 },
16 | { "image/tiff", 1 },
17 | { "image/gif", 1 },
18 | { "image/apng", 0.9f },
19 | { "image/png", 0.9f },
20 | { "image/webp", 0.9f },
21 | { "image/avif", 0.8f }
22 | };
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/CompressedStaticFiles.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | 2.2.0
6 | Peter Andersson;Andrey Kudashkin;Mathias Raacke;Arian Kadkhoda;Ilya Palkin;Mark Preston
7 | Peter Andersson;Andrey Kudashkin;Mathias Raacke;Arian Kadkhoda;Ilya Palkin;Mark Preston
8 | Send compressed static files to the browser without having to compress on demand, also has support for sending more advanced image formats when the browser indicates that i has support for it.
9 | Files need to be compressed and converted to other image formats before deploying.
10 | https://github.com/AnderssonPeter/CompressedStaticFiles
11 | aspnetcore;staticfiles;compression;precompressed;gzip;brotli;zopfli;webp;avif;Zstandard;zst
12 | 2.2.0
13 | Added support for Zstandard compression
14 | 2.1.0
15 | Added support for space in filenames
16 | Added support for RequestPath
17 | Added support for .net 6.0
18 | Removed support for .net 5.0
19 | Removed support for .net 3.1
20 | 2.0.0
21 | Added support for alternative image formats
22 | Added support for .net 5
23 | Removed support for .net core 2.1
24 | 1.2.0
25 | Added support for .net core 3.1
26 | 1.1.0
27 | New feature: Ability to specify a custom FileProvider (now using the FileProvider provided in the StaticFileOptions if it is set)
28 | 1.0.4
29 | Converted to .NET Standard 1.6
30 | 1.0.3
31 | Fixed issue that disabled caching parameters.
32 | 1.0.2
33 | Added support for IIS.
34 | 1.0.1
35 | Added logging
36 | Picks original file if its the smallest
37 | Added support for .NETFramework 4.5.1
38 |
39 | 1.0.0
40 | Initial release
41 | 2.1.0.0
42 | LICENSE
43 | 2.0.0.0
44 | true
45 | snupkg
46 | https://github.com/AnderssonPeter/CompressedStaticFiles
47 | git
48 | icon.png
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | True
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/IAlternativeFileProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.StaticFiles;
3 | using Microsoft.Extensions.FileProviders;
4 | #if NETSTANDARD2_0
5 | #else
6 | using IHost = Microsoft.AspNetCore.Hosting.IWebHostEnvironment;
7 | #endif
8 |
9 |
10 | namespace CompressedStaticFiles
11 | {
12 | public interface IAlternativeFileProvider
13 | {
14 | void Initialize(FileExtensionContentTypeProvider fileExtensionContentTypeProvider);
15 | IFileAlternative GetAlternative(HttpContext context, IFileProvider fileSystem, IFileInfo originalFile, PathString filePath);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/IFileAlternative.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.StaticFiles;
3 |
4 | namespace CompressedStaticFiles
5 | {
6 | public interface IFileAlternative
7 | {
8 | long Size { get; }
9 | ///
10 | /// Used to give some files a higher priority
11 | ///
12 | float Cost { get; }
13 | void Apply(HttpContext context);
14 | void Prepare(IContentTypeProvider contentTypeProvider, StaticFileResponseContext staticFileResponseContext);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/LoggerExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using System;
3 |
4 | namespace CompressedStaticFiles
5 | {
6 | internal static class LoggerExtensions
7 | {
8 | private static Action _logFileServed;
9 |
10 | static LoggerExtensions()
11 | {
12 | _logFileServed = LoggerMessage.Define(
13 | logLevel: LogLevel.Information,
14 | eventId: 1,
15 | formatString: "Sending file. Request file: '{RequestedPath}'. Served file: '{ServedPath}'. Original file size: {OriginalFileSize}. Served file size: {ServedFileSize}");
16 | }
17 |
18 | public static void LogFileServed(this ILogger logger, string requestedPath, string servedPath, long originalFileSize, long servedFileSize)
19 | {
20 | if (string.IsNullOrEmpty(requestedPath))
21 | {
22 | throw new ArgumentNullException(nameof(requestedPath));
23 | }
24 | if (string.IsNullOrEmpty(servedPath))
25 | {
26 | throw new ArgumentNullException(nameof(servedPath));
27 | }
28 | _logFileServed(logger, requestedPath, servedPath, originalFileSize, servedFileSize, null);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/CompressedStaticFiles/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/CompressedStaticFiles/icon.png
--------------------------------------------------------------------------------
/Example/Example.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0;
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Example/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.Logging;
10 |
11 | namespace Example
12 | {
13 | public class Program
14 | {
15 | public static void Main(string[] args)
16 | {
17 | CreateWebHostBuilder(args).Build().Run();
18 | }
19 |
20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
21 | WebHost.CreateDefaultBuilder(args)
22 | .UseStartup();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Example/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:50257",
7 | "sslPort": 0
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Development"
16 | }
17 | },
18 | "Example": {
19 | "commandName": "Project",
20 | "launchBrowser": true,
21 | "applicationUrl": "http://localhost:5000",
22 | "environmentVariables": {
23 | "ASPNETCORE_ENVIRONMENT": "Development"
24 | }
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/Example/Startup.cs:
--------------------------------------------------------------------------------
1 | using CompressedStaticFiles;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.AspNetCore.Http;
4 | using Microsoft.Extensions.Configuration;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace Example
8 | {
9 | public class Startup
10 | {
11 | public Startup(IConfiguration configuration)
12 | {
13 | Configuration = configuration;
14 | }
15 |
16 | public IConfiguration Configuration { get; }
17 |
18 | public void ConfigureServices(IServiceCollection services)
19 | {
20 | services.AddCompressedStaticFiles();
21 | }
22 |
23 | public void Configure(IApplicationBuilder app)
24 | {
25 | app.UseDefaultFiles();
26 | app.UseDeveloperExceptionPage();
27 | app.UseCompressedStaticFiles();
28 | app.UseSpa(spa =>
29 | {
30 | spa.Options.SourcePath = "wwwroot";
31 | });
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Example/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Example/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning"
5 | }
6 | },
7 | "AllowedHosts": "*"
8 | }
9 |
--------------------------------------------------------------------------------
/Example/gulpfile.js:
--------------------------------------------------------------------------------
1 | ///
2 | /*
3 | This file in the main entry point for defining Gulp tasks and using Gulp plugins.
4 | Click here to learn more. http://go.microsoft.com/fwlink/?LinkId=518007
5 | */
6 |
7 | var gulp = require('gulp'),
8 | brotli = require('gulp-brotli'),
9 | zopfli = require('gulp-zopfli-green'),
10 | zlib = require('zlib'),
11 | imagemin = require('gulp-imagemin'),
12 | imageminZopfli = require('imagemin-zopfli'),
13 | exec = require('gulp-exec');
14 |
15 | var compressPaths =
16 | ['wwwroot/**/*.js',
17 | 'wwwroot/**/*.html',
18 | 'wwwroot/**/*.htm',
19 | 'wwwroot/**/*.css',
20 | 'wwwroot/**/*.svg',
21 | 'wwwroot/**/*.ico'];
22 |
23 | var dest = 'wwwroot';
24 |
25 | const copy = () =>
26 | gulp.src(
27 | ['node_modules/bootstrap/dist/css/bootstrap.min.css',
28 | 'node_modules/bootstrap/dist/css/bootstrap-grid.min.css',
29 | 'node_modules/bootstrap/dist/css/bootstrap-reboot.min.css',
30 | 'node_modules/bootstrap/dist/js/bootstrap.min.js',
31 | 'images/*.*'])
32 | .pipe(gulp.dest('wwwroot/'));
33 |
34 | const compressGZip = () =>
35 | gulp.src(compressPaths)
36 | .pipe(zopfli())
37 | .pipe(gulp.dest(dest));
38 |
39 | const compressBrotli = () =>
40 | gulp.src(compressPaths)
41 | .pipe(brotli({
42 | params: {
43 | [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
44 | }
45 | }))
46 | .pipe(gulp.dest(dest));
47 |
48 | const compressZopfliPng = () =>
49 | gulp.src(['wwwroot/**/*.png'])
50 | .pipe(imagemin({
51 | use: [imageminZopfli()]
52 | }))
53 | .pipe(gulp.dest(dest));
54 |
55 | var options = {
56 | continueOnError: false, // default = false, true means don't emit error event
57 | pipeStdout: false, // default = false, true means stdout is written to file.contents
58 | };
59 |
60 | var reportOptions = {
61 | err: true, // default = true, false means don't write err
62 | stderr: true, // default = true, false means don't write stderr
63 | stdout: true // default = true, false means don't write stdout
64 | };
65 |
66 | const compressPngToWebP = () =>
67 | gulp.src(['wwwroot/**/*.png'])
68 | .pipe(exec(file => `cwebp -lossless -exact -m 6 -z 9 ${file.path} -o ${file.path.substring(0, file.path.length - 3)}webp`, options))
69 | .pipe(exec.reporter(reportOptions));
70 |
71 | //Skip Avif for png images as webp seems to always give smaller files.
72 | const compressPngToAvif = () =>
73 | gulp.src(['wwwroot/**/*.png'])
74 | .pipe(exec(file => `avifenc --lossless -s 0 ${file.path} -o ${file.path.substring(0, file.path.length - 3)}avif`, options))
75 | .pipe(exec.reporter(reportOptions));
76 |
77 | const compressPng = gulp.series(compressZopfliPng, compressPngToWebP);
78 |
79 | const compressJpegToWebP = () =>
80 | gulp.src(['wwwroot/**/*.jpg'])
81 | .pipe(exec(file => `cwebp -preset photo -q 95 -m 6 ${file.path} -o ${file.path.substring(0, file.path.length - 3)}webp`, options))
82 | .pipe(exec.reporter(reportOptions));
83 |
84 | const compressJpegToAvif = () =>
85 | gulp.src(['wwwroot/**/*.jpg'])
86 | .pipe(exec(file => `avifenc -s 0 --min 5 --max 15 ${file.path} -o ${file.path.substring(0, file.path.length - 3)}avif`, options))
87 | .pipe(exec.reporter(reportOptions));
88 |
89 | const compressJpeg = gulp.series(compressJpegToWebP, compressJpegToAvif);
90 |
91 | exports.build = gulp.series(copy, gulp.parallel(compressJpeg, compressGZip, compressBrotli, compressPng));
92 | exports.buildWithOutImages = gulp.series(copy, gulp.parallel(compressGZip, compressBrotli));
--------------------------------------------------------------------------------
/Example/images/IMG_6067.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/images/IMG_6067.jpg
--------------------------------------------------------------------------------
/Example/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/images/favicon.ico
--------------------------------------------------------------------------------
/Example/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/images/favicon.png
--------------------------------------------------------------------------------
/Example/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/images/icon.png
--------------------------------------------------------------------------------
/Example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "compressstaticfiles",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "gulp build",
8 | "buildWithOutImages": "gulp buildWithOutImages"
9 | },
10 | "author": "Peter Andersson",
11 | "license": "Apache-2.0",
12 | "devDependencies": {
13 | "gulp": "^4.0.2",
14 | "gulp-brotli": "^3.0.0",
15 | "gulp-zopfli-green": "^5.0.1",
16 | "imagemin-zopfli": "^7.0.0"
17 | },
18 | "dependencies": {
19 | "bootstrap": "^4.5.3",
20 | "gulp-exec": "^5.0.0",
21 | "gulp-imagemin": "^8.0.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Example/wwwroot/IMG_6067.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/wwwroot/IMG_6067.avif
--------------------------------------------------------------------------------
/Example/wwwroot/IMG_6067.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/wwwroot/IMG_6067.jpg
--------------------------------------------------------------------------------
/Example/wwwroot/IMG_6067.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/wwwroot/IMG_6067.webp
--------------------------------------------------------------------------------
/Example/wwwroot/cover.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Globals
3 | */
4 |
5 | /* Links */
6 | a,
7 | a:focus,
8 | a:hover {
9 | color: #fff;
10 | }
11 |
12 | /* Custom default button */
13 | .btn-secondary,
14 | .btn-secondary:hover,
15 | .btn-secondary:focus {
16 | color: #333;
17 | text-shadow: none; /* Prevent inheritance from `body` */
18 | background-color: #fff;
19 | border: .05rem solid #fff;
20 | }
21 |
22 |
23 | /*
24 | * Base structure
25 | */
26 |
27 | html,
28 | body {
29 | height: 100%;
30 | background-color: #333;
31 | }
32 |
33 | body {
34 | display: -ms-flexbox;
35 | display: flex;
36 | color: #fff;
37 | text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
38 | box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
39 | }
40 |
41 | .cover-container {
42 | max-width: 42em;
43 | }
44 |
45 | /*
46 | * Header
47 | */
48 | .masthead {
49 | margin-bottom: 2rem;
50 | }
51 |
52 | .masthead-brand {
53 | margin-bottom: 0;
54 | }
55 |
56 | .nav-masthead .nav-link {
57 | padding: .25rem 0;
58 | font-weight: 700;
59 | color: rgba(255, 255, 255, .5);
60 | background-color: transparent;
61 | border-bottom: .25rem solid transparent;
62 | }
63 |
64 | .nav-masthead .nav-link:hover,
65 | .nav-masthead .nav-link:focus {
66 | border-bottom-color: rgba(255, 255, 255, .25);
67 | }
68 |
69 | .nav-masthead .nav-link + .nav-link {
70 | margin-left: 1rem;
71 | }
72 |
73 | .nav-masthead .active {
74 | color: #fff;
75 | border-bottom-color: #fff;
76 | }
77 |
78 | @media (min-width: 48em) {
79 | .masthead-brand {
80 | float: left;
81 | }
82 |
83 | .nav-masthead {
84 | float: right;
85 | }
86 | }
87 |
88 |
89 | /*
90 | * Cover
91 | */
92 | .cover {
93 | padding: 0 1.5rem;
94 | }
95 |
96 | .cover .btn-lg {
97 | padding: .75rem 1.25rem;
98 | font-weight: 700;
99 | }
100 |
101 |
102 | /*
103 | * Footer
104 | */
105 | .mastfoot {
106 | color: rgba(255, 255, 255, .5);
107 | }
108 |
--------------------------------------------------------------------------------
/Example/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/Example/wwwroot/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/wwwroot/favicon.png
--------------------------------------------------------------------------------
/Example/wwwroot/favicon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/wwwroot/favicon.webp
--------------------------------------------------------------------------------
/Example/wwwroot/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/wwwroot/icon.png
--------------------------------------------------------------------------------
/Example/wwwroot/icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/6764afcf7c1eda34f657aa1d0f57daec8e0ce7f4/Example/wwwroot/icon.webp
--------------------------------------------------------------------------------
/Example/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CompressedStaticFiles Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
24 |
25 |
26 |
27 | Test page.
28 | Sample page to test the nuget package CompressedStaticFiles for ASP.NET 6
29 |
30 | nuget package
31 |
32 | Lighthouse mobile performance went from 76 to 98 by just applying the package on this page, it solves both Enable text compression and Serve images in next-gen formats
33 | The number of bytes transferred went from 526 kb to 141 kb .
34 |
35 |
36 |
41 |
42 |
43 |
44 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Example/wwwroot/style.css:
--------------------------------------------------------------------------------
1 | #logo {
2 | width: 80px;
3 | }
4 |
5 | body {
6 | background: linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('IMG_6067.jpg');
7 | background-size: cover;
8 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
CompressedStaticFiles
7 |
8 |
9 | Send compressed static files to the browser without having to compress on demand, also has support for sending more advanced image formats when the browser has support for it.
10 |
11 |
12 | ·
13 | Report Bug
14 | ·
15 | Request Feature
16 | ·
17 |
18 |
19 |
20 |
21 | [](https://www.nuget.org/packages/CompressedStaticFiles)
22 | [](https://www.nuget.org/packages/CompressedStaticFiles)
23 | [](https://github.com/AnderssonPeter/CompressedStaticFiles/actions?query=workflow%3A%22run+unit+tests%22)
24 | [](https://coveralls.io/github/AnderssonPeter/CompressedStaticFiles)
25 | [](https://raw.githubusercontent.com/AnderssonPeter/CompressedStaticFiles/master/LICENSE)
26 |
27 | ## Table of Contents
28 | * [About the Project](#about-the-project)
29 | * [Getting Started](#getting-started)
30 | * [Example](#example)
31 | * [Acknowledgements](#acknowledgements)
32 |
33 | ## About The Project
34 | This project allows you to serve precompressed files to the browser without having to compress on demand, this is achieved by compressing/encoding your content at build time.
35 |
36 | ## Getting Started
37 |
38 | ### Precompress content
39 | Static nonimage files have to be precompressed using [Zopfli](https://en.wikipedia.org/wiki/Zopfli), [Brotli](https://en.wikipedia.org/wiki/Brotli) and/or [ZStandard](https://en.wikipedia.org/wiki/Zstd), see the example for how to do it with gulp.
40 | The files must have the exact same filename as the source + `.br` or `.gzip` (`index.html` would be `index.html.br` for the Brotli version and `index.html.zst` for Zstandard).
41 |
42 | ### Encode images
43 | Modern browsers support new image formats like webp and avif they can store more pixels per byte.
44 | You can convert your images using the following tools [webp](https://developers.google.com/speed/webp/download) and [libavif](https://github.com/AOMediaCodec/libavif).
45 | The files must have the same filename as the source but with a new file extension (`image.jpg` would be `image.webp` for the webp version).
46 |
47 | ### ASP.NET
48 | Add `AddCompressedStaticFiles()` in your `Startup.ConfigureServices()` method.
49 | Replace `UseStaticFiles();` with `UseCompressedStaticFiles();` in `Startup.Configure()`.
50 | By default CompressedStaticFiles is configured to allow slightly larger files for some image formats as they can store more pixels per byte, this can be disabled by calling `CompressedStaticFileOptions.RemoveImageSubstitutionCostRatio()`.
51 |
52 | ## Example
53 | A example can be found in the [Example](https://github.com/AnderssonPeter/CompressedStaticFiles/tree/master/Example) directory.
54 | By using this package the Lighthouse mobile performance went from `76` to `98` and the transferred size went from `526 kb` to `141 kb`.
55 |
56 | ## Acknowledgements
57 | This solution is based on @neyromant from the following issue https://github.com/aspnet/Home/issues/1584#issuecomment-227455026.
58 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {"projects":["src","test"]}
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
17 |
19 |
42 |
44 |
45 |
47 | image/svg+xml
48 |
50 |
51 |
52 |
53 |
54 |
59 |
61 |
69 |
77 |
85 |
93 |
94 |
98 |
99 |
100 |
--------------------------------------------------------------------------------