diff --git a/.editorconfig b/.editorconfig
index 33fd0577..2e3045fb 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,5 +1,5 @@
-# Version: 2.1.0 (Using https://semver.org/)
-# Updated: 2021-03-03
+# Version: 4.1.1 (Using https://semver.org/)
+# Updated: 2022-05-23
# See https://github.com/RehanSaeed/EditorConfig/releases for release notes.
# See https://github.com/RehanSaeed/EditorConfig for updates to this file.
# See http://EditorConfig.org for more information about .editorconfig files.
@@ -49,11 +49,11 @@ indent_size = 2
indent_size = 2
# Markdown Files
-[*.md]
+[*.{md,mdx}]
trim_trailing_whitespace = false
# Web Files
-[*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,svg,vue}]
+[*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}]
indent_size = 2
# Batch Files
@@ -75,7 +75,7 @@ indent_style = tab
[*.{cs,csx,cake,vb,vbx}]
# Default Severity for all .NET Code Style rules below
-dotnet_analyzer_diagnostic.category-style.severity = warning
+dotnet_analyzer_diagnostic.severity = warning
##########################################
# Language Rules
@@ -122,20 +122,21 @@ dotnet_style_coalesce_expression = true:warning
dotnet_style_null_propagation = true:warning
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
# File header preferences
-file_header_template = Copyright (c) Six Labors.\nLicensed under the Apache License, Version 2.0.
+file_header_template = Copyright (c) Six Labors.\nLicensed under the Six Labors Split License.
# SA1636: File header copyright text should match
# Justification: .editorconfig supports file headers. If this is changed to a value other than "none", a stylecop.json file will need to added to the project.
# dotnet_diagnostic.SA1636.severity = none
# Undocumented
-dotnet_style_operator_placement_when_wrapping = end_of_line
+dotnet_style_operator_placement_when_wrapping = end_of_line:warning
+csharp_style_prefer_null_check_over_type_check = true:warning
# C# Style Rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#c-style-rules
[*.{cs,csx,cake}]
# 'var' preferences
-csharp_style_var_for_built_in_types = never
-csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_for_built_in_types = false:warning
+csharp_style_var_when_type_is_apparent = false:warning
csharp_style_var_elsewhere = false:warning
# Expression-bodied members
csharp_style_expression_bodied_methods = true:warning
@@ -200,12 +201,15 @@ dotnet_diagnostic.IDE0059.severity = suggestion
# Organize using directives
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false
+# Dotnet namespace options
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_diagnostic.IDE0130.severity = suggestion
# C# formatting rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#c-formatting-rules
[*.{cs,csx,cake}]
# Newline options
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#new-line-options
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#new-line-options
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
@@ -214,7 +218,7 @@ csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation options
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#indentation-options
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#indentation-options
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = no_change
@@ -222,7 +226,7 @@ csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents_when_block = false
# Spacing options
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#spacing-options
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#spacing-options
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_between_parentheses = false
@@ -246,9 +250,12 @@ csharp_space_before_open_square_brackets = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_square_brackets = false
# Wrap options
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#wrap-options
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#wrap-options
csharp_preserve_single_line_statements = false
csharp_preserve_single_line_blocks = true
+# Namespace options
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#namespace-options
+csharp_style_namespace_declarations = file_scoped:warning
##########################################
# .NET Naming Rules
diff --git a/.gitattributes b/.gitattributes
index 70ced690..ff4ec940 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -64,18 +64,19 @@
# Set explicit file behavior to:
# treat as text
# normalize to Unix-style line endings and
-# use a union merge when resoling conflicts
+# use a union merge when resolving conflicts
###############################################################################
*.csproj text eol=lf merge=union
*.dbproj text eol=lf merge=union
*.fsproj text eol=lf merge=union
*.ncrunchproject text eol=lf merge=union
*.vbproj text eol=lf merge=union
+*.shproj text eol=lf merge=union
###############################################################################
# Set explicit file behavior to:
# treat as text
# normalize to Windows-style line endings and
-# use a union merge when resoling conflicts
+# use a union merge when resolving conflicts
###############################################################################
*.sln text eol=crlf merge=union
###############################################################################
@@ -87,7 +88,6 @@
*.eot binary
*.exe binary
*.otf binary
-*.pbm binary
*.pdf binary
*.ppt binary
*.pptx binary
@@ -95,7 +95,6 @@
*.snk binary
*.ttc binary
*.ttf binary
-*.wbmp binary
*.woff binary
*.woff2 binary
*.xls binary
@@ -126,3 +125,10 @@
*.dds filter=lfs diff=lfs merge=lfs -text
*.ktx filter=lfs diff=lfs merge=lfs -text
*.ktx2 filter=lfs diff=lfs merge=lfs -text
+*.pam filter=lfs diff=lfs merge=lfs -text
+*.pbm filter=lfs diff=lfs merge=lfs -text
+*.pgm filter=lfs diff=lfs merge=lfs -text
+*.ppm filter=lfs diff=lfs merge=lfs -text
+*.pnm filter=lfs diff=lfs merge=lfs -text
+*.wbmp filter=lfs diff=lfs merge=lfs -text
+*.exr filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 8d32fbfc..3ca7e152 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,4 +1,4 @@
-# How to contribute to ImageSharp.Web
+# How to contribute to SixLabors.ImageSharp.Web
#### **Did you find a bug?**
@@ -16,20 +16,18 @@
#### **Do you intend to add a new feature or change an existing one?**
-* Suggest your change in the [ImageSharp Gitter Chat Room](https://gitter.im/ImageSharp/General) and start writing code.
+* Suggest your change in the [Ideas Discussions Channel](https://github.com/SixLabors/ImageSharp.Web/discussions?discussions_q=category%3AIdeas) and start writing code.
* Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes.
#### **Do you have questions about consuming the library or the source code?**
-* Ask any question about how to use ImageSharp in the [ImageSharp Gitter Chat Room](https://gitter.im/ImageSharp/General).
+* Ask any question about how to use SixLabors.ImageSharp.Web in the [Help Discussions Channel](https://github.com/SixLabors/ImageSharp.Web/discussions?discussions_q=category%3AHelp).
-#### Code of Conduct
+#### Code of Conduct
This project has adopted the code of conduct defined by the [Contributor Covenant](https://contributor-covenant.org/) to clarify expected behavior in our community.
For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
-And please remember. ImageSharp.Web is the work of a very, very, small number of developers who struggle balancing time to contribute to the project with family time and work commitments. We encourage you to pitch in and help make our vision of simple accessible imageprocessing available to all. Open Source can only exist with your help.
+And please remember. SixLabors.ImageSharp.Web is the work of a very, very, small number of developers who struggle balancing time to contribute to the project with family time and work commitments. We encourage you to pitch in and help make our vision of simple accessible image processing available to all. Open Source can only exist with your help.
Thanks for reading!
-
-James Jackson-South :heart:
diff --git a/.github/ISSUE_TEMPLATE/commercial-bug-report.md b/.github/ISSUE_TEMPLATE/commercial-bug-report.md
deleted file mode 100644
index 3a4fafd0..00000000
--- a/.github/ISSUE_TEMPLATE/commercial-bug-report.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-name: "Commercial License : Bug Report"
-about: |
- Create a report to help us improve the project. For Commercial License holders only.
- Please contact help@sixlabors.com for issues requiring private support.
-labels: commercial, needs triage
-
----
-
-
-### Prerequisites
-
-- [ ] I have written a descriptive issue title
-- [ ] I have verified that I am running the latest version of ImageSharp.Web
-- [ ] I have verified if the problem exist in both `DEBUG` and `RELEASE` mode
-- [ ] I have searched [open](https://github.com/SixLabors/ImageSharp.Web/issues) and [closed](https://github.com/SixLabors/ImageSharp.Web/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported
-
-### Description
-
-
-### Steps to Reproduce
-
-
-### System Configuration
-
-
-- ImageSharp.Web version:
-- Other Six Labors packages and versions:
-- Environment (Operating system, version and so on):
-- .NET Framework version:
-- Additional information:
-
-
diff --git a/.github/ISSUE_TEMPLATE/oss-bug-report.md b/.github/ISSUE_TEMPLATE/oss-bug-report.md
deleted file mode 100644
index 2195dce1..00000000
--- a/.github/ISSUE_TEMPLATE/oss-bug-report.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-name: "OSS : Bug Report"
-about: Create a report to help us improve the project.
-labels: needs triage
-
----
-
-### Prerequisites
-
-- [ ] I have written a descriptive issue title
-- [ ] I have verified that I am running the latest version of ImageSharp.Web
-- [ ] I have verified if the problem exist in both `DEBUG` and `RELEASE` mode
-- [ ] I have searched [open](https://github.com/SixLabors/ImageSharp.Web/issues) and [closed](https://github.com/SixLabors/ImageSharp.Web/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported
-
-### Description
-
-
-### Steps to Reproduce
-
-
-### System Configuration
-
-
-- ImageSharp.Web version:
-- Other Six Labors packages and versions:
-- Environment (Operating system, version and so on):
-- .NET Framework version:
-- Additional information:
-
-
diff --git a/.github/ISSUE_TEMPLATE/oss-bug-report.yml b/.github/ISSUE_TEMPLATE/oss-bug-report.yml
new file mode 100644
index 00000000..020bef57
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/oss-bug-report.yml
@@ -0,0 +1,52 @@
+name: "Bug Report"
+description: Create a report to help us improve the project. Issues are not guaranteed to be triaged.
+labels: ["needs triage"]
+body:
+- type: checkboxes
+ attributes:
+ label: Prerequisites
+ options:
+ - label: I have written a descriptive issue title
+ required: true
+ - label: I have verified that I am running the latest version of ImageSharp.Web
+ required: true
+ - label: I have verified if the problem exist in both `DEBUG` and `RELEASE` mode
+ required: true
+ - label: I have searched [open](https://github.com/SixLabors/ImageSharp.Web/issues) and [closed](https://github.com/SixLabors/ImageSharp.Web/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported
+ required: true
+- type: input
+ attributes:
+ label: ImageSharp.Web version
+ validations:
+ required: true
+- type: input
+ attributes:
+ label: Other Six Labors packages and versions
+ validations:
+ required: true
+- type: input
+ attributes:
+ label: Environment (Operating system, version and so on)
+ validations:
+ required: true
+- type: input
+ attributes:
+ label: .NET version
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: Description
+ description: A description of the bug
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: Steps to Reproduce
+ description: List of steps, sample code, failing test or link to a project that reproduces the behavior. Make sure you place a stack trace inside a code (```) block to avoid linking unrelated issues.
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: Images
+ description: Please upload images that can be used to reproduce issues in the area below. If the file type is not supported the file can be zipped and then uploaded instead.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..5ace4600
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 97005fd2..d323d270 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -1,137 +1,203 @@
name: Build
on:
- push:
- branches:
- - master
- tags:
- - "v*"
- pull_request:
- branches:
- - master
+ push:
+ branches:
+ - main
+ tags:
+ - "v*"
+ pull_request:
+ branches:
+ - main
jobs:
- Build:
- strategy:
- matrix:
- options:
- - os: macos-latest
- framework: netcoreapp3.1
- runtime: -x64
- codecov: false
- - os: ubuntu-latest
- framework: netcoreapp3.1
- runtime: -x64
- codecov: false
- - os: windows-latest
- framework: netcoreapp3.1
- runtime: -x64
- codecov: true
- - os: windows-latest
- framework: netcoreapp2.1
- runtime: -x64
- codecov: false
-
- runs-on: ${{matrix.options.os}}
- if: "!contains(github.event.head_commit.message, '[skip ci]')"
-
- steps:
- - uses: actions/checkout@v2
-
- # See https://github.com/actions/checkout/issues/165#issuecomment-657673315
- - name: Create LFS file list
- run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
-
- - name: Restore LFS cache
- uses: actions/cache@v2
- id: lfs-cache
- with:
- path: .git/lfs
- key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
-
- - name: Git LFS Pull
- run: git lfs pull
-
- - name: Install NuGet
- uses: NuGet/setup-nuget@v1
-
- - name: Setup Git
- shell: bash
- run: |
- git config --global core.autocrlf false
- git config --global core.longpaths true
- git fetch --prune --unshallow
- git submodule -q update --init --recursive
-
- - name: Setup Azurite
- if: matrix.options.os != 'windows-latest'
- shell: bash
- run: |
- sudo npm install -g azurite
- sudo azurite --loose &
-
- - name: Setup Azurite Windows
- if: matrix.options.os == 'windows-latest'
- shell: bash
- run: |
- npm install -g azurite
- azurite --loose &
-
- - name: Setup NuGet Cache
- uses: actions/cache@v2
- id: nuget-cache
- with:
- path: ~/.nuget
- key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
- restore-keys: ${{ runner.os }}-nuget-
-
- - name: Build
- shell: pwsh
- run: ./ci-build.ps1 "${{matrix.options.framework}}"
- env:
- SIXLABORS_TESTING: True
-
- - name: Test
- shell: pwsh
- run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
- env:
- SIXLABORS_TESTING: True
- XUNIT_PATH: .\tests\ImageSharp.Web.Tests # Required for xunit
-
- - name: Update Codecov
- uses: codecov/codecov-action@v1
- if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
- with:
- flags: unittests
-
- Publish:
- needs: [Build]
-
- runs-on: windows-latest
-
- if: (github.event_name == 'push')
-
- steps:
- - uses: actions/checkout@v2
-
- - name: Install NuGet
- uses: NuGet/setup-nuget@v1
-
- - name: Setup Git
- shell: bash
- run: |
- git config --global core.autocrlf false
- git config --global core.longpaths true
- git fetch --prune --unshallow
- git submodule -q update --init --recursive
-
- - name: Pack
- shell: pwsh
- run: ./ci-pack.ps1
-
- - name: Publish to MyGet
- shell: pwsh
- run: |
- nuget.exe push .\artifacts\*.nupkg ${{secrets.MYGET_TOKEN}} -Source https://www.myget.org/F/sixlabors/api/v2/package
- nuget.exe push .\artifacts\*.snupkg ${{secrets.MYGET_TOKEN}} -Source https://www.myget.org/F/sixlabors/api/v3/index.json
- # TODO: If github.ref starts with 'refs/tags' then it was tag push and we can optionally push out package to nuget.org
+ Build:
+ strategy:
+ matrix:
+ options:
+ - os: ubuntu-latest
+ framework: net7.0
+ sdk-preview: true
+ runtime: -x64
+ codecov: false
+ - os: macos-latest
+ framework: net7.0
+ sdk-preview: true
+ runtime: -x64
+ codecov: false
+ - os: windows-latest
+ framework: net7.0
+ sdk-preview: true
+ runtime: -x64
+ codecov: true
+ - os: ubuntu-latest
+ framework: net6.0
+ runtime: -x64
+ codecov: false
+ - os: macos-latest
+ framework: net6.0
+ runtime: -x64
+ codecov: false
+ - os: windows-latest
+ framework: net6.0
+ runtime: -x64
+ codecov: false
+
+ runs-on: ${{matrix.options.os}}
+
+ steps:
+ - name: Git Config
+ shell: bash
+ run: |
+ git config --global core.autocrlf false
+ git config --global core.longpaths true
+
+ - name: Git Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ # See https://github.com/actions/checkout/issues/165#issuecomment-657673315
+ - name: Git Create LFS FileList
+ run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
+
+ - name: Git Setup LFS Cache
+ uses: actions/cache@v4
+ id: lfs-cache
+ with:
+ path: .git/lfs
+ key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
+
+ - name: Git Pull LFS
+ run: git lfs pull
+
+ - name: NuGet Install
+ uses: NuGet/setup-nuget@v2
+
+ - name: NuGet Setup Cache
+ uses: actions/cache@v4
+ id: nuget-cache
+ with:
+ path: ~/.nuget
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
+ restore-keys: ${{ runner.os }}-nuget-
+
+ - name: Azurite Setup
+ if: matrix.options.os != 'windows-latest'
+ shell: bash
+ run: |
+ sudo npm install -g azurite
+ sudo azurite --loose --skipApiVersionCheck &
+
+ - name: Azurite Setup Windows
+ if: matrix.options.os == 'windows-latest'
+ shell: bash
+ run: |
+ npm install -g azurite
+ azurite --loose --skipApiVersionCheck &
+
+ - name: S3rver Setup
+ if: matrix.options.os != 'windows-latest'
+ shell: bash
+ run: |
+ sudo npm install -g s3rver
+ sudo s3rver -d . &
+
+ - name: S3rver Setup Windows
+ if: matrix.options.os == 'windows-latest'
+ shell: bash
+ run: |
+ npm install -g s3rver
+ s3rver -d . &
+
+ - name: DotNet Setup
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 7.0.x
+ 6.0.x
+
+ - name: DotNet Build
+ if: ${{ matrix.options.sdk-preview != true }}
+ shell: pwsh
+ run: ./ci-build.ps1 "${{matrix.options.framework}}"
+ env:
+ SIXLABORS_TESTING: True
+
+ - name: DotNet Build Preview
+ if: ${{ matrix.options.sdk-preview == true }}
+ shell: pwsh
+ run: ./ci-build.ps1 "${{matrix.options.framework}}"
+ env:
+ SIXLABORS_TESTING_PREVIEW: True
+
+ - name: DotNet Test
+ if: ${{ matrix.options.sdk-preview != true }}
+ shell: pwsh
+ run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
+ env:
+ SIXLABORS_TESTING: True
+ XUNIT_PATH: .\tests\ImageSharp.Web.Tests # Required for xunit
+
+ - name: DotNet Test Preview
+ if: ${{ matrix.options.sdk-preview == true }}
+ shell: pwsh
+ run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
+ env:
+ SIXLABORS_TESTING_PREVIEW: True
+ XUNIT_PATH: .\tests\ImageSharp.Web.Tests # Required for xunit
+
+ - name: Codecov Update
+ uses: codecov/codecov-action@v4
+ if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
+ with:
+ flags: unittests
+
+ Publish:
+ needs: [Build]
+
+ runs-on: ubuntu-latest
+
+ if: (github.event_name == 'push')
+
+ steps:
+ - name: Git Config
+ shell: bash
+ run: |
+ git config --global core.autocrlf false
+ git config --global core.longpaths true
+
+ - name: Git Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ - name: NuGet Install
+ uses: NuGet/setup-nuget@v2
+
+ - name: NuGet Setup Cache
+ uses: actions/cache@v4
+ id: nuget-cache
+ with:
+ path: ~/.nuget
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
+ restore-keys: ${{ runner.os }}-nuget-
+
+ - name: DotNet Pack
+ shell: pwsh
+ run: ./ci-pack.ps1
+
+ - name: Feedz Publish
+ shell: pwsh
+ run: |
+ dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.FEEDZ_TOKEN}} -s https://f.feedz.io/sixlabors/sixlabors/nuget/index.json --skip-duplicate
+ dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.FEEDZ_TOKEN}} -s https://f.feedz.io/sixlabors/sixlabors/symbols --skip-duplicate
+
+ - name: NuGet Publish
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
+ shell: pwsh
+ run: |
+ dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
+ dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
diff --git a/.gitignore b/.gitignore
index 0e6ad770..da9deced 100644
--- a/.gitignore
+++ b/.gitignore
@@ -134,7 +134,7 @@ publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
-# TODO: Comment the next line if you want to checkin your web deploy settings
+# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
@@ -214,7 +214,7 @@ artifacts/
*.csproj.bak
#CodeCoverage
-*.lcov
+*.lcov
**/is-cache/
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index b34bbb41..365f69e2 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,3 +1,2 @@
-# Code of Conduct
+# Code of Conduct
This project has adopted the code of conduct defined by the [Contributor Covenant](https://contributor-covenant.org/) to clarify expected behavior in our community.
-For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
\ No newline at end of file
diff --git a/ImageSharp.Web.sln b/ImageSharp.Web.sln
index f74b7478..8c81ddff 100644
--- a/ImageSharp.Web.sln
+++ b/ImageSharp.Web.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.28803.452
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.33424.131
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}"
ProjectSection(SolutionItems) = preProject
@@ -11,10 +11,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1
ci-build.ps1 = ci-build.ps1
ci-pack.ps1 = ci-pack.ps1
ci-test.ps1 = ci-test.ps1
- .github\CONTRIBUTING.md = .github\CONTRIBUTING.md
+ CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
+ LICENSE = LICENSE
README.md = README.md
+ SixLabors.ImageSharp.Web.props = SixLabors.ImageSharp.Web.props
shared-infrastructure\SixLabors.ruleset = shared-infrastructure\SixLabors.ruleset
shared-infrastructure\SixLabors.Tests.ruleset = shared-infrastructure\SixLabors.Tests.ruleset
shared-infrastructure\stylecop.json = shared-infrastructure\stylecop.json
@@ -40,15 +42,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Web.Benchmarks",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Web.Providers.Azure", "src\ImageSharp.Web.Providers.Azure\ImageSharp.Web.Providers.Azure.csproj", "{E2A545EC-B909-4EAD-B95F-397F68588BE3}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Web.Providers.AWS", "src\ImageSharp.Web.Providers.AWS\ImageSharp.Web.Providers.AWS.csproj", "{E631D300-ACD5-40EA-A6BB-08E22092EC76}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{B8152C86-B657-4967-B297-42F89A10D23A}"
+ ProjectSection(SolutionItems) = preProject
+ .github\CONTRIBUTING.md = .github\CONTRIBUTING.md
+ .github\dependabot.yml = .github\dependabot.yml
+ .github\FUNDING.yml = .github\FUNDING.yml
+ .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md
+ EndProjectSection
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedInfrastructure", "shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.shproj", "{68A8CC40-6AED-4E96-B524-31B1158FDEEA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{04955CD1-F249-4899-9F61-6C7487BEECE1}"
ProjectSection(SolutionItems) = preProject
- .github\ISSUE_TEMPLATE\commercial-bug-report.md = .github\ISSUE_TEMPLATE\commercial-bug-report.md
.github\ISSUE_TEMPLATE\config.yml = .github\ISSUE_TEMPLATE\config.yml
- .github\ISSUE_TEMPLATE\oss-bug-report.md = .github\ISSUE_TEMPLATE\oss-bug-report.md
+ .github\ISSUE_TEMPLATE\oss-bug-report.yml = .github\ISSUE_TEMPLATE\oss-bug-report.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{2A5EF4AC-E228-42C1-970B-35630A9AF250}"
@@ -65,11 +74,6 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Web.Sample", "samples\ImageSharp.Web.Sample\ImageSharp.Web.Sample.csproj", "{8F40DEC6-A97F-4002-A504-13E188665A98}"
EndProject
Global
- GlobalSection(SharedMSBuildProjectFiles) = preSolution
- shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2f1b36e2-5d92-4442-b816-d2a978246435}*SharedItemsImports = 5
- shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13
- shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{e2a545ec-b909-4ead-b95f-397f68588be3}*SharedItemsImports = 5
- EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
@@ -127,6 +131,18 @@ Global
{E2A545EC-B909-4EAD-B95F-397F68588BE3}.Release|x64.Build.0 = Release|Any CPU
{E2A545EC-B909-4EAD-B95F-397F68588BE3}.Release|x86.ActiveCfg = Release|Any CPU
{E2A545EC-B909-4EAD-B95F-397F68588BE3}.Release|x86.Build.0 = Release|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Debug|x64.Build.0 = Debug|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Debug|x86.Build.0 = Debug|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Release|x64.ActiveCfg = Release|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Release|x64.Build.0 = Release|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Release|x86.ActiveCfg = Release|Any CPU
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76}.Release|x86.Build.0 = Release|Any CPU
{8F40DEC6-A97F-4002-A504-13E188665A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F40DEC6-A97F-4002-A504-13E188665A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F40DEC6-A97F-4002-A504-13E188665A98}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -148,6 +164,7 @@ Global
{8864C96C-94AA-454D-BAF5-779216C3745A} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{0B15E490-7821-42DF-86A5-4DEAE921DE59} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{E2A545EC-B909-4EAD-B95F-397F68588BE3} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
+ {E631D300-ACD5-40EA-A6BB-08E22092EC76} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
{B8152C86-B657-4967-B297-42F89A10D23A} = {C317F1B1-D75E-4C6D-83EB-80367343E0D7}
{68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
{04955CD1-F249-4899-9F61-6C7487BEECE1} = {B8152C86-B657-4967-B297-42F89A10D23A}
@@ -157,4 +174,10 @@ Global
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C5B38B65-A19E-4359-859C-5B2205429BD1}
EndGlobalSection
+ GlobalSection(SharedMSBuildProjectFiles) = preSolution
+ shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2f1b36e2-5d92-4442-b816-d2a978246435}*SharedItemsImports = 5
+ shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13
+ shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{e2a545ec-b909-4ead-b95f-397f68588be3}*SharedItemsImports = 5
+ shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{e631d300-acd5-40ea-a6bb-08e22092ec76}*SharedItemsImports = 5
+ EndGlobalSection
EndGlobal
diff --git a/LICENSE b/LICENSE
index 8d5852d3..a68eb678 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,201 +1,43 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
+Six Labors Split License
+Version 1.0, June 2022
+Copyright (c) Six Labors
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
- 1. Definitions.
+1. Definitions.
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source
+ code, documentation source, and configuration files.
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including
+ but not limited to compiled object code, generated documentation, and conversions to other media types.
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
+ "Work" (or "Works") shall mean any Six Labors software made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work.
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
+ "Direct Package Dependency" shall mean any Work in Source or Object form that is installed directly by You.
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
+ "Transitive Package Dependency" shall mean any Work in Object form that is installed indirectly by a third party
+ dependency unrelated to Six Labors.
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
+2. License
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
+ Works in Source or Object form are split licensed and may be licensed under the Apache License, Version 2.0 or a
+ Six Labors Commercial Use License.
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
+ Licenses are granted based upon You meeting the qualified criteria as stated. Once granted,
+ You must reference the granted license only in all documentation.
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
+ Works in Source or Object form are licensed to You under the Apache License, Version 2.0 if.
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
+ - You are consuming the Work in for use in software licensed under an Open Source or Source Available license.
+ - You are consuming the Work as a Transitive Package Dependency.
+ - You are consuming the Work as a Direct Package Dependency in the capacity of a For-profit company/individual with
+ less than 1M USD annual gross revenue.
+ - You are consuming the Work as a Direct Package Dependency in the capacity of a Non-profit organization
+ or Registered Charity.
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright (c) Six Labors
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+ For all other scenarios, Works in Source or Object form are licensed to You under the Six Labors Commercial License
+ which may be purchased by visiting https://sixlabors.com/pricing/.
diff --git a/README.md b/README.md
index 4e382e7b..5b80433b 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,15 @@
-### **ImageSharp.Web** is a new high-performance ASP.NET Core middleware leveraging the ImageSharp graphics library to allow on-the-fly image manipulation via URL based commands.
+### **ImageSharp.Web** is a high-performance ASP.NET Core middleware leveraging the ImageSharp graphics library to allow on-the-fly image manipulation via URL based commands.
## License
-- ImageSharp.Web is licensed under the [Apache License, Version 2.0](https://opensource.org/licenses/Apache-2.0)
-- An alternative Commercial Support License can be purchased **for projects and applications requiring support**.
-Please visit https://sixlabors.com/pricing for details.
+- ImageSharp.Web is licensed under the [Six Labors Split License, Version 1.0](https://github.com/SixLabors/ImageSharp.Web/blob/main/LICENSE)
## Support Six Labors
@@ -38,7 +36,7 @@ Support the efforts of the development of the Six Labors projects.
## Questions
- Do you have questions? We are happy to help! Please [join our Discussions Forum](https://github.com/SixLabors/ImageSharp/discussions/category_choices), or ask them on [Stack Overflow](https://stackoverflow.com) using the `ImageSharp.Web` tag. Please do not open issues for questions.
-- Please read our [Contribution Guide](https://github.com/SixLabors/ImageSharp.Web/blob/master/.github/CONTRIBUTING.md) before opening issues or pull requests!
+- Please read our [Contribution Guide](https://github.com/SixLabors/ImageSharp.Web/blob/main/.github/CONTRIBUTING.md) before opening issues or pull requests!
## Code of Conduct
@@ -47,19 +45,19 @@ For more information, see the [.NET Foundation Code of Conduct](https://dotnetfo
### Installation
-Install stable releases via Nuget; development releases are available via MyGet.
+Install stable releases via Nuget; development releases are available via Feedz.io.
-| Package Name | Release (NuGet) | Nightly (MyGet) |
+| Package Name | Release (NuGet) | Nightly (Feedz.io) |
|--------------------------------|-----------------|-----------------|
-| `SixLabors.ImageSharp.Web` | [](https://www.nuget.org/packages/SixLabors.ImageSharp.Web/) | [](https://www.myget.org/feed/sixlabors/package/nuget/SixLabors.ImageSharp.Web) |
+| `SixLabors.ImageSharp.Web` | [](https://www.nuget.org/packages/SixLabors.ImageSharp.Web/) | [](https://f.feedz.io/sixlabors/sixlabors/nuget/index.json) |
## Manual build
If you prefer, you can compile ImageSharp.Web yourself (please do and help!)
-- Using [Visual Studio 2019](https://visualstudio.microsoft.com/vs/)
+- Using [Visual Studio 2022](https://visualstudio.microsoft.com/vs/)
- Make sure you have the latest version installed
- - Make sure you have [the .NET Core 3.1 SDK](https://www.microsoft.com/net/core#windows) installed
+ - Make sure you have [the .NET 6 SDK](https://www.microsoft.com/net/core#windows) installed
Alternatively, you can work from command line and/or with a lightweight editor on **both Linux/Unix and Windows**:
@@ -86,25 +84,31 @@ git submodule update --init --recursive
#### Running the Tests
-The unit tests require [Azurite Azure Storage Emulator](https://github.com/Azure/Azurite) in order to run.
+The unit tests require [Azurite Azure Storage Emulator](https://github.com/Azure/Azurite) and [s3rver](https://github.com/jamhall/s3rver) in order to run.
On Windows to install and run the server as a background process run the following command
```bash
npm install -g azurite
-start /B azurite --loose
+start /B azurite --loose --skipApiVersionCheck
+
+npm install -g s3rver
+start /B s3rver -d .
```
On Linux
```bash
sudo npm install -g azurite
-sudo azurite --loose &
+sudo azurite --loose --skipApiVersionCheck &
+
+sudo npm install -g s3rver
+sudo s3rver -d . &
```
## How can you help?
-Please... Spread the word, contribute algorithms, submit performance improvements, unit tests, no input is too little. Make sure to read our [Contribution Guide](https://github.com/SixLabors/ImageSharp.Web/blob/master/.github/CONTRIBUTING.md) before opening a PR.
+Please... Spread the word, contribute algorithms, submit performance improvements, unit tests, no input is too little. Make sure to read our [Contribution Guide](https://github.com/SixLabors/ImageSharp.Web/blob/main/.github/CONTRIBUTING.md) before opening a PR.
## The ImageSharp.Web Team
@@ -112,4 +116,4 @@ Please... Spread the word, contribute algorithms, submit performance improvement
- [Dirk Lemstra](https://github.com/dlemstra)
- [Anton Firsov](https://github.com/antonfirsov)
- [Scott Williams](https://github.com/tocsoft)
-- [Brian Popow](https://github.com/brianpopow)
\ No newline at end of file
+- [Brian Popow](https://github.com/brianpopow)
diff --git a/SixLabors.ImageSharp.Web.props b/SixLabors.ImageSharp.Web.props
new file mode 100644
index 00000000..1f42eb91
--- /dev/null
+++ b/SixLabors.ImageSharp.Web.props
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ci-pack.ps1 b/ci-pack.ps1
index 09f45347..55c69fb5 100644
--- a/ci-pack.ps1
+++ b/ci-pack.ps1
@@ -3,4 +3,4 @@ dotnet clean -c Release
$repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY"
# Building for packing and publishing.
-dotnet pack -c Release --output "$PSScriptRoot/artifacts" /p:RepositoryUrl=$repositoryUrl
+dotnet pack -c Release -p:PackageOutputPath="$PSScriptRoot/artifacts" -p:RepositoryUrl=$repositoryUrl
diff --git a/codecov.yml b/codecov.yml
index 833fc0a5..310eefb8 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -9,3 +9,14 @@ codecov:
# Avoid Report Expired
# https://docs.codecov.io/docs/codecov-yaml#section-expired-reports
max_report_age: off
+
+coverage:
+ # Use integer precision
+ # https://docs.codecov.com/docs/codecovyml-reference#coverageprecision
+ precision: 0
+
+ # Explicitly control coverage status checks
+ # https://docs.codecov.com/docs/commit-status#disabling-a-status
+ status:
+ project: on
+ patch: off
diff --git a/samples/ImageSharp.Web.Sample/ImageSharp.Web.Sample.csproj b/samples/ImageSharp.Web.Sample/ImageSharp.Web.Sample.csproj
index 744de963..d20146da 100644
--- a/samples/ImageSharp.Web.Sample/ImageSharp.Web.Sample.csproj
+++ b/samples/ImageSharp.Web.Sample/ImageSharp.Web.Sample.csproj
@@ -1,10 +1,16 @@
- netcoreapp3.1
+ net6.0
+ enable
+ enable
+ 10
+
+
+
diff --git a/samples/ImageSharp.Web.Sample/Pages/Index.cshtml b/samples/ImageSharp.Web.Sample/Pages/Index.cshtml
new file mode 100644
index 00000000..73ad597b
--- /dev/null
+++ b/samples/ImageSharp.Web.Sample/Pages/Index.cshtml
@@ -0,0 +1,1237 @@
+@page
+@model IndexModel
+@{
+ ViewData["Title"] = "ImageSharp URI API Samples";
+}
+
+@section Css {
+
+}
+
+
Demonstrates that the middleware handles identical queries without file contention.
-
-
- imagesharp-logo.png?width=123
-
-
-
-
-
-
-
- imagesharp-logo.png?width=123
-
-
-
-
-
-
-
- imagesharp-logo.png?width=123
-
-
-
-
-
-
-
-
-
diff --git a/samples/ImageSharp.Web.Sample/wwwroot/sixlabors.imagesharp.web.png b/samples/ImageSharp.Web.Sample/wwwroot/sixlabors.imagesharp.web.png
new file mode 100644
index 00000000..d60a4f9c
--- /dev/null
+++ b/samples/ImageSharp.Web.Sample/wwwroot/sixlabors.imagesharp.web.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e320603ed2376bf6c79180b0e611b08a1c11c4b68856327023ef4af323a55628
+size 18945
diff --git a/samples/ImageSharp.Web.Sample/wwwroot/sixlabors.imagesharp.web.svg b/samples/ImageSharp.Web.Sample/wwwroot/sixlabors.imagesharp.web.svg
new file mode 100644
index 00000000..b1ff27a5
--- /dev/null
+++ b/samples/ImageSharp.Web.Sample/wwwroot/sixlabors.imagesharp.web.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shared-infrastructure b/shared-infrastructure
index a042aba1..f0d7ed20 160000
--- a/shared-infrastructure
+++ b/shared-infrastructure
@@ -1 +1 @@
-Subproject commit a042aba176cdb840d800c6ed4cfe41a54fb7b1e3
+Subproject commit f0d7ed20b36ab1f9e379ca3bee528e6efd991b00
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 0e2e54a6..c6b0ceed 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -21,6 +21,11 @@
true
+
+
+ 2.0
+
+
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
index 4782c198..f5811d15 100644
--- a/src/Directory.Build.targets
+++ b/src/Directory.Build.targets
@@ -17,19 +17,16 @@
-
-
-
-
-
+
+
diff --git a/src/ImageSharp.Web.Providers.AWS/AmazonS3BucketClient.cs b/src/ImageSharp.Web.Providers.AWS/AmazonS3BucketClient.cs
new file mode 100644
index 00000000..d8940666
--- /dev/null
+++ b/src/ImageSharp.Web.Providers.AWS/AmazonS3BucketClient.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using Amazon.S3;
+
+namespace SixLabors.ImageSharp.Web;
+
+///
+/// Represents a scoped Amazon S3 client instance that is explicitly associated with a single S3 bucket.
+/// This wrapper provides a strongly-typed link between the client and the bucket it operates on,
+/// and optionally manages the lifetime of the underlying .
+///
+public sealed class AmazonS3BucketClient : IDisposable
+{
+ private readonly bool disposeClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The bucket name associated with this client instance.
+ ///
+ ///
+ /// The underlying Amazon S3 client instance. This should be an already configured instance of .
+ ///
+ ///
+ /// A value indicating whether the underlying client should be disposed when this instance is disposed.
+ ///
+ public AmazonS3BucketClient(string bucketName, AmazonS3Client client, bool disposeClient = true)
+ {
+ Guard.NotNullOrWhiteSpace(bucketName, nameof(bucketName));
+ Guard.NotNull(client, nameof(client));
+ this.BucketName = bucketName;
+ this.Client = client;
+ this.disposeClient = disposeClient;
+ }
+
+ ///
+ /// Gets the bucket name associated with this client instance.
+ ///
+ public string BucketName { get; }
+
+ ///
+ /// Gets the underlying Amazon S3 client instance.
+ ///
+ public AmazonS3Client Client { get; }
+
+ ///
+ public void Dispose()
+ {
+ if (this.disposeClient)
+ {
+ this.Client.Dispose();
+ }
+ }
+}
diff --git a/src/ImageSharp.Web.Providers.AWS/AmazonS3ClientFactory.cs b/src/ImageSharp.Web.Providers.AWS/AmazonS3ClientFactory.cs
new file mode 100644
index 00000000..ff21b56a
--- /dev/null
+++ b/src/ImageSharp.Web.Providers.AWS/AmazonS3ClientFactory.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using Amazon;
+using Amazon.Runtime;
+using Amazon.S3;
+
+namespace SixLabors.ImageSharp.Web;
+
+internal static class AmazonS3ClientFactory
+{
+ ///
+ /// Creates a new bucket under the specified account if a bucket
+ /// with the same name does not already exist.
+ ///
+ /// The AWS S3 Storage cache options.
+ ///
+ /// A new .
+ ///
+ /// Invalid configuration.
+ public static AmazonS3BucketClient CreateClient(IAWSS3BucketClientOptions options)
+ {
+ if (!string.IsNullOrWhiteSpace(options.Endpoint))
+ {
+ // AccessKey can be empty.
+ // AccessSecret can be empty.
+ // PathStyle endpoint doesn't support AccelerateEndpoint.
+ AmazonS3Config config = new() { ServiceURL = options.Endpoint, ForcePathStyle = true, AuthenticationRegion = options.Region };
+ SetTimeout(config, options.Timeout);
+ return new(options.BucketName, new AmazonS3Client(options.AccessKey, options.AccessSecret, config));
+ }
+ else if (!string.IsNullOrWhiteSpace(options.AccessKey))
+ {
+ // AccessSecret can be empty.
+ Guard.NotNullOrWhiteSpace(options.Region, nameof(options.Region));
+ RegionEndpoint region = RegionEndpoint.GetBySystemName(options.Region);
+ AmazonS3Config config = new() { RegionEndpoint = region, UseAccelerateEndpoint = options.UseAccelerateEndpoint };
+ SetTimeout(config, options.Timeout);
+ return new(options.BucketName, new AmazonS3Client(options.AccessKey, options.AccessSecret, config));
+ }
+ else if (!string.IsNullOrWhiteSpace(options.Region))
+ {
+ RegionEndpoint region = RegionEndpoint.GetBySystemName(options.Region);
+ AmazonS3Config config = new() { RegionEndpoint = region, UseAccelerateEndpoint = options.UseAccelerateEndpoint };
+ SetTimeout(config, options.Timeout);
+ return new(options.BucketName, new AmazonS3Client(config));
+ }
+ else
+ {
+ throw new ArgumentException("Invalid configuration.", nameof(options));
+ }
+ }
+
+ private static void SetTimeout(ClientConfig config, TimeSpan? timeout)
+ {
+ // We don't want to override the default timeout if it's not set.
+ if (timeout.HasValue)
+ {
+ config.Timeout = timeout.Value;
+ }
+ }
+}
diff --git a/src/ImageSharp.Web.Providers.AWS/Caching/AWSS3StorageCache.cs b/src/ImageSharp.Web.Providers.AWS/Caching/AWSS3StorageCache.cs
new file mode 100644
index 00000000..aef41ac7
--- /dev/null
+++ b/src/ImageSharp.Web.Providers.AWS/Caching/AWSS3StorageCache.cs
@@ -0,0 +1,206 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Globalization;
+using Amazon.S3;
+using Amazon.S3.Model;
+using Microsoft.Extensions.Options;
+using SixLabors.ImageSharp.Web.Resolvers;
+using SixLabors.ImageSharp.Web.Resolvers.AWS;
+
+namespace SixLabors.ImageSharp.Web.Caching.AWS;
+
+///
+/// Implements an AWS S3 Storage based cache.
+///
+public class AWSS3StorageCache : IImageCache, IDisposable
+{
+ private readonly AmazonS3BucketClient amazonS3Client;
+ private readonly string bucketName;
+ private readonly string cacheFolder;
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The cache options.
+ /// The current service provider.
+ public AWSS3StorageCache(IOptions cacheOptions, IServiceProvider serviceProvider)
+ {
+ Guard.NotNull(cacheOptions, nameof(cacheOptions));
+ AWSS3StorageCacheOptions options = cacheOptions.Value;
+
+ this.amazonS3Client =
+ options.S3ClientFactory?.Invoke(options, serviceProvider)
+ ?? AmazonS3ClientFactory.CreateClient(options);
+
+ this.bucketName = this.amazonS3Client.BucketName;
+
+ this.cacheFolder = string.IsNullOrEmpty(options.CacheFolder)
+ ? string.Empty
+ : options.CacheFolder.Trim().Trim('/') + '/';
+ }
+
+ ///
+ public async Task GetAsync(string key)
+ {
+ string keyWithFolder = this.GetKeyWithFolder(key);
+ GetObjectMetadataRequest request = new() { BucketName = this.bucketName, Key = keyWithFolder };
+ try
+ {
+ // HEAD request throws a 404 if not found.
+ MetadataCollection metadata = (await this.amazonS3Client.Client.GetObjectMetadataAsync(request)).Metadata;
+ return new AWSS3StorageCacheResolver(this.amazonS3Client.Client, this.bucketName, keyWithFolder, metadata);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
+ {
+ PutObjectRequest request = new()
+ {
+ BucketName = this.bucketName,
+ Key = this.GetKeyWithFolder(key),
+ ContentType = metadata.ContentType,
+ InputStream = stream,
+ AutoCloseStream = false,
+ UseChunkEncoding = false
+ };
+
+ foreach (KeyValuePair d in metadata.ToDictionary())
+ {
+ request.Metadata.Add(d.Key, d.Value);
+ }
+
+ return this.amazonS3Client.Client.PutObjectAsync(request);
+ }
+
+ ///
+ /// Creates a new bucket under the specified account if a bucket
+ /// with the same name does not already exist.
+ ///
+ /// The AWS S3 Storage cache options.
+ ///
+ /// Specifies whether data in the bucket may be accessed publicly and the level of access.
+ /// specifies full public read access for bucket
+ /// and object data. specifies that the bucket
+ /// data is private to the account owner.
+ ///
+ ///
+ /// If the bucket does not already exist, a describing the newly
+ /// created bucket. If the container already exists, .
+ ///
+ public static PutBucketResponse? CreateIfNotExists(AWSS3StorageCacheOptions options, S3CannedACL acl)
+ => AsyncHelper.RunSync(() => CreateIfNotExistsAsync(options, acl));
+
+ ///
+ /// Releases the unmanaged resources used by the and optionally releases the managed resources.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!this.isDisposed)
+ {
+ if (disposing)
+ {
+ this.amazonS3Client?.Dispose();
+ }
+
+ this.isDisposed = true;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ this.Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ private static async Task CreateIfNotExistsAsync(AWSS3StorageCacheOptions options, S3CannedACL acl)
+ {
+ using AmazonS3BucketClient bucketClient = AmazonS3ClientFactory.CreateClient(options);
+ AmazonS3Client client = bucketClient.Client;
+
+ bool foundBucket = false;
+ ListBucketsResponse listBucketsResponse = await client.ListBucketsAsync();
+ foreach (S3Bucket b in listBucketsResponse.Buckets)
+ {
+ if (b.BucketName == options.BucketName)
+ {
+ foundBucket = true;
+ break;
+ }
+ }
+
+ if (!foundBucket)
+ {
+ PutBucketRequest putBucketRequest = new()
+ {
+ BucketName = options.BucketName,
+ BucketRegion = options.Region,
+ CannedACL = acl
+ };
+
+ return await client.PutBucketAsync(putBucketRequest);
+ }
+
+ return null;
+ }
+
+ private string GetKeyWithFolder(string key)
+ => this.cacheFolder + key;
+
+ ///
+ ///
+ ///
+ private static class AsyncHelper
+ {
+ private static readonly TaskFactory TaskFactory
+ = new(
+ CancellationToken.None,
+ TaskCreationOptions.None,
+ TaskContinuationOptions.None,
+ TaskScheduler.Default);
+
+ ///
+ /// Executes an async method synchronously.
+ ///
+ /// The task to execute.
+ public static void RunSync(Func task)
+ {
+ CultureInfo cultureUi = CultureInfo.CurrentUICulture;
+ CultureInfo culture = CultureInfo.CurrentCulture;
+ TaskFactory.StartNew(() =>
+ {
+ Thread.CurrentThread.CurrentCulture = culture;
+ Thread.CurrentThread.CurrentUICulture = cultureUi;
+ return task();
+ }).Unwrap().GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Executes an async method which has
+ /// a return type synchronously.
+ ///
+ /// The type of result to return.
+ /// The task to execute.
+ /// The .
+ public static TResult RunSync(Func> task)
+ {
+ CultureInfo cultureUi = CultureInfo.CurrentUICulture;
+ CultureInfo culture = CultureInfo.CurrentCulture;
+ return TaskFactory.StartNew(() =>
+ {
+ Thread.CurrentThread.CurrentCulture = culture;
+ Thread.CurrentThread.CurrentUICulture = cultureUi;
+ return task();
+ }).Unwrap().GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/ImageSharp.Web.Providers.AWS/Caching/AWSS3StorageCacheOptions.cs b/src/ImageSharp.Web.Providers.AWS/Caching/AWSS3StorageCacheOptions.cs
new file mode 100644
index 00000000..e9c4a606
--- /dev/null
+++ b/src/ImageSharp.Web.Providers.AWS/Caching/AWSS3StorageCacheOptions.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Web.Caching.AWS;
+
+///
+/// Configuration options for the provider.
+///
+public class AWSS3StorageCacheOptions : IAWSS3BucketClientOptions
+{
+ ///
+ public Func? S3ClientFactory { get; set; }
+
+ ///
+ public string? Region { get; set; }
+
+ ///
+ public string BucketName { get; set; } = null!;
+
+ ///
+ /// Gets or sets the cache folder's name that'll store cache files under the configured bucket.
+ ///
+ public string? CacheFolder { get; set; }
+
+ ///
+ public string? AccessKey { get; set; }
+
+ ///
+ public string? AccessSecret { get; set; }
+
+ ///
+ public string? Endpoint { get; set; }
+
+ ///
+ public bool UseAccelerateEndpoint { get; set; }
+
+ ///
+ public TimeSpan? Timeout { get; set; }
+}
diff --git a/src/ImageSharp.Web.Providers.AWS/IAWSS3BucketClientOptions.cs b/src/ImageSharp.Web.Providers.AWS/IAWSS3BucketClientOptions.cs
new file mode 100644
index 00000000..62bd79fe
--- /dev/null
+++ b/src/ImageSharp.Web.Providers.AWS/IAWSS3BucketClientOptions.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Web;
+
+///
+/// Provides a common interface for AWS S3 Bucket Client Options.
+///
+public interface IAWSS3BucketClientOptions
+{
+ ///
+ /// Gets or sets a custom factory method to create an .
+ ///
+ public Func? S3ClientFactory { get; set; }
+
+ ///
+ /// Gets or sets the AWS region endpoint (us-east-1/us-west-1/ap-southeast-2).
+ ///
+ public string? Region { get; set; }
+
+ ///
+ /// Gets or sets the AWS bucket name.
+ /// Cannot be when is not set.
+ ///
+ public string BucketName { get; set; }
+
+ ///
+ /// Gets or sets the AWS key - Can be used to override keys provided by the environment.
+ /// If deploying inside an EC2 instance AWS keys will already be available via environment
+ /// variables and don't need to be specified. Follow AWS best security practices on
+ /// .
+ ///
+ public string? AccessKey { get; set; }
+
+ ///
+ /// Gets or sets the AWS endpoint - used to override the default service endpoint.
+ /// If deploying inside an EC2 instance AWS keys will already be available via environment
+ /// variables and don't need to be specified. Follow AWS best security practices on
+ /// .
+ ///
+ public string? AccessSecret { get; set; }
+
+ ///
+ /// Gets or sets the AWS endpoint - used for testing to over region endpoint allowing it
+ /// to be set to localhost.
+ ///
+ public string? Endpoint { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the S3 accelerate endpoint is used.
+ /// The feature must be enabled on the bucket. Follow AWS instruction on
+ /// .
+ ///
+ public bool UseAccelerateEndpoint { get; set; }
+
+ ///
+ /// Gets or sets a value indicating the timeout for the S3 client.
+ /// If the value is set, the value is assigned to the Timeout property of the HttpWebRequest/HttpClient
+ /// object used to send requests.
+ ///
+ public TimeSpan? Timeout { get; set; }
+}
diff --git a/src/ImageSharp.Web.Providers.AWS/ImageSharp.Web.Providers.AWS.csproj b/src/ImageSharp.Web.Providers.AWS/ImageSharp.Web.Providers.AWS.csproj
new file mode 100644
index 00000000..41a35fa7
--- /dev/null
+++ b/src/ImageSharp.Web.Providers.AWS/ImageSharp.Web.Providers.AWS.csproj
@@ -0,0 +1,55 @@
+
+
+
+ SixLabors.ImageSharp.Web.Providers.AWS
+ SixLabors.ImageSharp.Web.Providers.AWS
+ SixLabors.ImageSharp.Web
+ SixLabors.ImageSharp.Web.Providers.AWS
+ sixlabors.imagesharp.web.128.png
+ LICENSE
+ https://github.com/SixLabors/ImageSharp.Web/
+ $(RepositoryUrl)
+ Image Middleware Resize Crop Gif Jpg Jpeg Bitmap Png AWS
+ A provider for resolving and caching images via AWS S3 Storage.
+
+
+
+
+ enable
+ Nullable
+
+
+
+
+ 3.0
+
+
+
+
+
+ net7.0;net6.0
+
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ImageSharp.Web.Providers.AWS/Providers/AWSS3StorageImageProvider.cs b/src/ImageSharp.Web.Providers.AWS/Providers/AWSS3StorageImageProvider.cs
new file mode 100644
index 00000000..20ab9b15
--- /dev/null
+++ b/src/ImageSharp.Web.Providers.AWS/Providers/AWSS3StorageImageProvider.cs
@@ -0,0 +1,223 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using Amazon.S3;
+using Amazon.S3.Model;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Extensions.Options;
+using SixLabors.ImageSharp.Web.Resolvers;
+using SixLabors.ImageSharp.Web.Resolvers.AWS;
+
+namespace SixLabors.ImageSharp.Web.Providers.AWS;
+
+///
+/// Returns images stored in AWS S3.
+///
+public class AWSS3StorageImageProvider : IImageProvider, IDisposable
+{
+ ///
+ /// Character array to remove from paths.
+ ///
+ private static readonly char[] SlashChars = { '\\', '/' };
+
+ ///
+ /// The containers for the blob services.
+ ///
+ private readonly Dictionary buckets
+ = new();
+
+ private readonly AWSS3StorageImageProviderOptions storageOptions;
+ private Func? match;
+ private bool isDisposed;
+
+ ///
+ /// Contains various helper methods based on the current configuration.
+ ///
+ private readonly FormatUtilities formatUtilities;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The S3 storage options
+ /// Contains various format helper methods based on the current configuration.
+ /// The current service provider.
+ public AWSS3StorageImageProvider(
+ IOptions storageOptions,
+ FormatUtilities formatUtilities,
+ IServiceProvider serviceProvider)
+ {
+ Guard.NotNull(storageOptions, nameof(storageOptions));
+
+ this.storageOptions = storageOptions.Value;
+
+ this.formatUtilities = formatUtilities;
+
+ foreach (AWSS3BucketClientOptions bucket in this.storageOptions.S3Buckets)
+ {
+ AmazonS3BucketClient s3Client =
+ bucket.S3ClientFactory?.Invoke(bucket, serviceProvider)
+ ?? AmazonS3ClientFactory.CreateClient(bucket);
+
+ this.buckets.Add(s3Client.BucketName, s3Client);
+ }
+ }
+
+ ///
+ public ProcessingBehavior ProcessingBehavior { get; } = ProcessingBehavior.All;
+
+ ///
+ public Func Match
+ {
+ get => this.match ?? this.IsMatch;
+ set => this.match = value;
+ }
+
+ ///
+ public bool IsValidRequest(HttpContext context)
+ => this.formatUtilities.TryGetExtensionFromUri(context.Request.GetDisplayUrl(), out _);
+
+ ///
+ public async Task GetAsync(HttpContext context)
+ {
+ // Strip the leading slash and bucket name from the HTTP request path and treat
+ // the remaining path string as the key.
+ // Path has already been correctly parsed before here.
+ string bucketName = string.Empty;
+ AmazonS3Client? s3Client = null;
+
+ // We want an exact match here to ensure that bucket names starting with
+ // the same prefix are not mixed up.
+ string? path = context.Request.Path.Value?.TrimStart(SlashChars);
+
+ if (path is null)
+ {
+ return null;
+ }
+
+ int index = path.IndexOfAny(SlashChars);
+ string nameToMatch = index != -1 ? path[..index] : path;
+
+ foreach (string k in this.buckets.Keys)
+ {
+ if (nameToMatch.Equals(k, StringComparison.OrdinalIgnoreCase))
+ {
+ bucketName = k;
+ s3Client = this.buckets[k].Client;
+ break;
+ }
+ }
+
+ // Something has gone horribly wrong for this to happen but check anyway.
+ if (s3Client is null)
+ {
+ return null;
+ }
+
+ // Key should be the remaining path string.
+ string key = path[bucketName.Length..].TrimStart(SlashChars);
+
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ return null;
+ }
+
+ KeyExistsResult keyExists = await KeyExists(s3Client, bucketName, key);
+ if (!keyExists.Exists)
+ {
+ return null;
+ }
+
+ return new AWSS3StorageImageResolver(s3Client, bucketName, key, keyExists.Metadata);
+ }
+
+ private bool IsMatch(HttpContext context)
+ {
+ // Only match loosely here for performance.
+ // Path matching conflicts should be dealt with by configuration.
+ string? path = context.Request.Path.Value?.TrimStart(SlashChars);
+
+ if (path is null)
+ {
+ return false;
+ }
+
+ foreach (string bucket in this.buckets.Keys)
+ {
+ if (path.StartsWith(bucket, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // ref https://github.com/aws/aws-sdk-net/blob/master/sdk/src/Services/S3/Custom/_bcl/IO/S3FileInfo.cs#L118
+ private static async Task KeyExists(IAmazonS3 s3Client, string bucketName, string key)
+ {
+ try
+ {
+ GetObjectMetadataRequest request = new() { BucketName = bucketName, Key = key };
+
+ // If the object doesn't exist then a "NotFound" will be thrown
+ GetObjectMetadataResponse metadata = await s3Client.GetObjectMetadataAsync(request);
+ return new KeyExistsResult(metadata);
+ }
+ catch (AmazonS3Exception e)
+ {
+ if (string.Equals(e.ErrorCode, "NoSuchBucket", StringComparison.Ordinal))
+ {
+ return default;
+ }
+
+ if (string.Equals(e.ErrorCode, "NotFound", StringComparison.Ordinal))
+ {
+ return default;
+ }
+
+ // If the object exists but the client is not authorized to access it, then a "Forbidden" will be thrown.
+ if (string.Equals(e.ErrorCode, "Forbidden", StringComparison.Ordinal))
+ {
+ return default;
+ }
+
+ throw;
+ }
+ }
+
+ ///
+ /// Releases the unmanaged resources used by the and optionally releases the managed resources.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!this.isDisposed)
+ {
+ if (disposing)
+ {
+ foreach (AmazonS3BucketClient client in this.buckets.Values)
+ {
+ client?.Dispose();
+ }
+
+ this.buckets.Clear();
+ }
+
+ this.isDisposed = true;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.Dispose(true);
+
+ GC.SuppressFinalize(this);
+ }
+
+ private readonly record struct KeyExistsResult(GetObjectMetadataResponse Metadata)
+ {
+ public bool Exists => this.Metadata is not null;
+ }
+}
diff --git a/src/ImageSharp.Web.Providers.AWS/Providers/AWSS3StorageImageProviderOptions.cs b/src/ImageSharp.Web.Providers.AWS/Providers/AWSS3StorageImageProviderOptions.cs
new file mode 100644
index 00000000..187aec7e
--- /dev/null
+++ b/src/ImageSharp.Web.Providers.AWS/Providers/AWSS3StorageImageProviderOptions.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Web.Providers.AWS;
+
+///
+/// Configuration options for the provider.
+///
+public class AWSS3StorageImageProviderOptions
+{
+ ///
+ /// Gets or sets the collection of blob container client options.
+ ///
+ public ICollection S3Buckets { get; set; } = new HashSet();
+}
+
+///
+/// Configuration options for the provider.
+///
+public class AWSS3BucketClientOptions : IAWSS3BucketClientOptions
+{
+ ///
+ public Func? S3ClientFactory { get; set; }
+
+ ///
+ public string? Region { get; set; }
+
+ ///
+ public string BucketName { get; set; } = null!;
+
+ ///
+ public string? AccessKey { get; set; }
+
+ ///
+ public string? AccessSecret { get; set; }
+
+ ///
+ public string? Endpoint { get; set; }
+
+ ///
+ public bool UseAccelerateEndpoint { get; set; }
+
+ ///
+ public TimeSpan? Timeout { get; set; }
+}
diff --git a/src/ImageSharp.Web.Providers.AWS/Resolvers/AWSS3StorageCacheResolver.cs b/src/ImageSharp.Web.Providers.AWS/Resolvers/AWSS3StorageCacheResolver.cs
new file mode 100644
index 00000000..2add992f
--- /dev/null
+++ b/src/ImageSharp.Web.Providers.AWS/Resolvers/AWSS3StorageCacheResolver.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using Amazon.S3;
+using Amazon.S3.Model;
+
+namespace SixLabors.ImageSharp.Web.Resolvers.AWS;
+
+///
+/// Provides means to manage image buffers within the .
+///
+public class AWSS3StorageCacheResolver : IImageCacheResolver
+{
+ private readonly IAmazonS3 amazonS3;
+ private readonly string bucketName;
+ private readonly string imagePath;
+ private readonly MetadataCollection metadata;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Amazon S3 Client
+ /// The bucket name.
+ /// The image path.
+ /// The metadata collection.
+ public AWSS3StorageCacheResolver(IAmazonS3 amazonS3, string bucketName, string imagePath, MetadataCollection metadata)
+ {
+ this.amazonS3 = amazonS3;
+ this.bucketName = bucketName;
+ this.imagePath = imagePath;
+ this.metadata = metadata;
+ }
+
+ ///
+ public Task GetMetaDataAsync()
+ {
+ Dictionary dict = new();
+ foreach (string key in this.metadata.Keys)
+ {
+ // Trim automatically added x-amz-meta-
+ dict.Add(key[11..].ToUpperInvariant(), this.metadata[key]);
+ }
+
+ return Task.FromResult(ImageCacheMetadata.FromDictionary(dict));
+ }
+
+ ///
+ public Task OpenReadAsync() => this.amazonS3.GetObjectStreamAsync(this.bucketName, this.imagePath, null);
+}
diff --git a/src/ImageSharp.Web.Providers.AWS/Resolvers/AWSS3StorageImageResolver.cs b/src/ImageSharp.Web.Providers.AWS/Resolvers/AWSS3StorageImageResolver.cs
new file mode 100644
index 00000000..42e042fd
--- /dev/null
+++ b/src/ImageSharp.Web.Providers.AWS/Resolvers/AWSS3StorageImageResolver.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Net.Http.Headers;
+using Amazon.S3;
+using Amazon.S3.Model;
+
+namespace SixLabors.ImageSharp.Web.Resolvers.AWS;
+
+///
+/// Provides means to manage image buffers within the AWS S3 file system.
+///
+public class AWSS3StorageImageResolver : IImageResolver
+{
+ private readonly IAmazonS3 amazonS3;
+ private readonly string bucketName;
+ private readonly string imagePath;
+ private readonly GetObjectMetadataResponse? metadataResponse;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Amazon S3 Client
+ /// The bucket name.
+ /// The image path.
+ /// Optional metadata response.
+ public AWSS3StorageImageResolver(IAmazonS3 amazonS3, string bucketName, string imagePath, GetObjectMetadataResponse? metadataResponse = null)
+ {
+ this.amazonS3 = amazonS3;
+ this.bucketName = bucketName;
+ this.imagePath = imagePath;
+ this.metadataResponse = metadataResponse;
+ }
+
+ ///
+ public async Task GetMetaDataAsync()
+ {
+ GetObjectMetadataResponse metadata = this.metadataResponse ?? await this.amazonS3.GetObjectMetadataAsync(this.bucketName, this.imagePath);
+
+ // Try to parse the max age from the source. If it's not zero then we pass it along
+ // to set the cache control headers for the response.
+ TimeSpan maxAge = TimeSpan.MinValue;
+ if (CacheControlHeaderValue.TryParse(metadata.Headers.CacheControl, out CacheControlHeaderValue? cacheControl))
+ {
+ // Weirdly passing null to TryParse returns true.
+ if (cacheControl?.MaxAge.HasValue == true)
+ {
+ maxAge = cacheControl.MaxAge.Value;
+ }
+ }
+
+ return new ImageMetadata(metadata.LastModified ?? DateTime.UtcNow, maxAge, metadata.ContentLength);
+ }
+
+ ///
+ public Task OpenReadAsync()
+ => this.amazonS3.GetObjectStreamAsync(this.bucketName, this.imagePath, null);
+}
diff --git a/src/ImageSharp.Web.Providers.Azure/Caching/AzureBlobStorageCache.cs b/src/ImageSharp.Web.Providers.Azure/Caching/AzureBlobStorageCache.cs
index d1224cab..e49d281a 100644
--- a/src/ImageSharp.Web.Providers.Azure/Caching/AzureBlobStorageCache.cs
+++ b/src/ImageSharp.Web.Providers.Azure/Caching/AzureBlobStorageCache.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System.IO;
-using System.Threading.Tasks;
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
@@ -10,76 +8,86 @@
using SixLabors.ImageSharp.Web.Resolvers;
using SixLabors.ImageSharp.Web.Resolvers.Azure;
-namespace SixLabors.ImageSharp.Web.Caching.Azure
+namespace SixLabors.ImageSharp.Web.Caching.Azure;
+
+///
+/// Implements an Azure Blob Storage based cache.
+///
+public class AzureBlobStorageCache : IImageCache
{
+ private readonly BlobContainerClient container;
+ private readonly string cacheFolder;
+
///
- /// Implements an Azure Blob Storage based cache.
+ /// Initializes a new instance of the class.
///
- public class AzureBlobStorageCache : IImageCache
+ /// The cache options.
+ /// The current service provider.
+ public AzureBlobStorageCache(IOptions cacheOptions, IServiceProvider serviceProvider)
{
- private readonly BlobContainerClient container;
+ Guard.NotNull(cacheOptions, nameof(cacheOptions));
+ AzureBlobStorageCacheOptions options = cacheOptions.Value;
- ///
- /// Initializes a new instance of the class.
- ///
- /// The cache options.
- public AzureBlobStorageCache(IOptions cacheOptions)
- {
- Guard.NotNull(cacheOptions, nameof(cacheOptions));
- AzureBlobStorageCacheOptions options = cacheOptions.Value;
+ this.container =
+ options.BlobContainerClientFactory?.Invoke(options, serviceProvider)
+ ?? new BlobContainerClient(options.ConnectionString, options.ContainerName);
- this.container = new BlobContainerClient(options.ConnectionString, options.ContainerName);
- }
+ this.cacheFolder = string.IsNullOrWhiteSpace(options.CacheFolder)
+ ? string.Empty
+ : options.CacheFolder.Trim().Trim('/') + '/';
+ }
+
+ ///
+ public async Task GetAsync(string key)
+ {
+ BlobClient blob = this.GetBlob(key);
- ///
- public async Task GetAsync(string key)
+ if (!await blob.ExistsAsync())
{
- BlobClient blob = this.container.GetBlobClient(key);
+ return null;
+ }
- if (!await blob.ExistsAsync())
- {
- return null;
- }
+ return new AzureBlobStorageCacheResolver(blob);
+ }
- return new AzureBlobStorageCacheResolver(blob);
- }
+ ///
+ public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
+ {
+ BlobClient blob = this.GetBlob(key);
- ///
- public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
+ BlobHttpHeaders headers = new()
{
- BlobClient blob = this.container.GetBlobClient(key);
+ ContentType = metadata.ContentType,
+ };
- var headers = new BlobHttpHeaders
- {
- ContentType = metadata.ContentType,
- };
+ return blob.UploadAsync(stream, httpHeaders: headers, metadata: metadata.ToDictionary());
+ }
- return blob.UploadAsync(stream, httpHeaders: headers, metadata: metadata.ToDictionary());
- }
+ ///
+ /// Creates a new container under the specified account if a container
+ /// with the same name does not already exist.
+ ///
+ /// The Azure Blob Storage cache options.
+ ///
+ /// Optionally specifies whether data in the container may be accessed publicly and
+ /// the level of access.
+ /// specifies full public read access for container and blob data. Clients can enumerate
+ /// blobs within the container via anonymous request, but cannot enumerate containers
+ /// within the storage account.
+ /// specifies public read access for blobs. Blob data within this container can be
+ /// read via anonymous request, but container data is not available. Clients cannot
+ /// enumerate blobs within the container via anonymous request.
+ /// specifies that the container data is private to the account owner.
+ ///
+ ///
+ /// If the container does not already exist, a describing the newly
+ /// created container. If the container already exists, .
+ ///
+ public static Response CreateIfNotExists(
+ AzureBlobStorageCacheOptions options,
+ PublicAccessType accessType)
+ => new BlobContainerClient(options.ConnectionString, options.ContainerName).CreateIfNotExists(accessType);
- ///
- /// Creates a new container under the specified account if a container
- /// with the same name does not already exist.
- ///
- /// The Azure Blob Storage cache options.
- ///
- /// Optionally specifies whether data in the container may be accessed publicly and
- /// the level of access.
- /// specifies full public read access for container and blob data. Clients can enumerate
- /// blobs within the container via anonymous request, but cannot enumerate containers
- /// within the storage account.
- /// specifies public read access for blobs. Blob data within this container can be
- /// read via anonymous request, but container data is not available. Clients cannot
- /// enumerate blobs within the container via anonymous request.
- /// specifies that the container data is private to the account owner.
- ///
- ///
- /// If the container does not already exist, a describing the newly
- /// created container. If the container already exists, .
- ///
- public static Response CreateIfNotExists(
- AzureBlobStorageCacheOptions options,
- PublicAccessType accessType)
- => new BlobContainerClient(options.ConnectionString, options.ContainerName).CreateIfNotExists(accessType);
- }
+ private BlobClient GetBlob(string key)
+ => this.container.GetBlobClient(this.cacheFolder + key);
}
diff --git a/src/ImageSharp.Web.Providers.Azure/Caching/AzureBlobStorageCacheOptions.cs b/src/ImageSharp.Web.Providers.Azure/Caching/AzureBlobStorageCacheOptions.cs
index 9d253d5d..ab119aa6 100644
--- a/src/ImageSharp.Web.Providers.Azure/Caching/AzureBlobStorageCacheOptions.cs
+++ b/src/ImageSharp.Web.Providers.Azure/Caching/AzureBlobStorageCacheOptions.cs
@@ -1,24 +1,37 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Web.Caching.Azure
+using Azure.Storage.Blobs;
+
+namespace SixLabors.ImageSharp.Web.Caching.Azure;
+
+///
+/// Configuration options for the .
+///
+public class AzureBlobStorageCacheOptions
{
///
- /// Configuration options for the .
+ /// Gets or sets a factory method to create an .
+ ///
+ public Func? BlobContainerClientFactory { get; set; }
+
+ ///
+ /// Gets or sets the Azure Blob Storage connection string.
+ ///
///
- public class AzureBlobStorageCacheOptions
- {
- ///
- /// Gets or sets the Azure Blob Storage connection string.
- ///
- ///
- public string ConnectionString { get; set; }
+ public string ConnectionString { get; set; } = null!;
- ///
- /// Gets or sets the Azure Blob Storage container name.
- /// Must conform to Azure Blob Storage containiner naming guidlines.
- ///
- ///
- public string ContainerName { get; set; }
- }
+ ///
+ /// Gets or sets the Azure Blob Storage container name.
+ /// Must conform to Azure Blob Storage container naming guidelines.
+ ///
+ ///
+ public string ContainerName { get; set; } = null!;
+
+ ///
+ /// Gets or sets the cache folder's name that'll store cache files under the configured container.
+ /// Must conform to Azure Blob Storage directory naming guidelines.
+ ///
+ ///
+ public string? CacheFolder { get; set; }
}
diff --git a/src/ImageSharp.Web.Providers.Azure/ImageSharp.Web.Providers.Azure.csproj b/src/ImageSharp.Web.Providers.Azure/ImageSharp.Web.Providers.Azure.csproj
index 0ceb2c21..da7e9305 100644
--- a/src/ImageSharp.Web.Providers.Azure/ImageSharp.Web.Providers.Azure.csproj
+++ b/src/ImageSharp.Web.Providers.Azure/ImageSharp.Web.Providers.Azure.csproj
@@ -1,4 +1,4 @@
-
+SixLabors.ImageSharp.Web.Providers.Azure
@@ -6,23 +6,44 @@
SixLabors.ImageSharp.WebSixLabors.ImageSharp.Web.Providers.Azuresixlabors.imagesharp.web.128.png
- Apache-2.0
+ LICENSEhttps://github.com/SixLabors/ImageSharp.Web/$(RepositoryUrl)Image Middleware Resize Crop Gif Jpg Jpeg Bitmap Png AzureA provider for resolving and caching images via Azure Blob Storage.
-
+
+
+
+
+ enable
+ Nullable
+
- netcoreapp3.1;netcoreapp2.1
+
+ 3.0
+
+
+
+ net7.0;net6.0
+
+
+
+
+ net6.0
+
+
+
+
+
-
+
-
+
diff --git a/src/ImageSharp.Web.Providers.Azure/Providers/AzureBlobStorageImageProvider.cs b/src/ImageSharp.Web.Providers.Azure/Providers/AzureBlobStorageImageProvider.cs
index 00b0ac5d..60069a55 100644
--- a/src/ImageSharp.Web.Providers.Azure/Providers/AzureBlobStorageImageProvider.cs
+++ b/src/ImageSharp.Web.Providers.Azure/Providers/AzureBlobStorageImageProvider.cs
@@ -1,9 +1,6 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
@@ -11,138 +8,147 @@
using SixLabors.ImageSharp.Web.Resolvers;
using SixLabors.ImageSharp.Web.Resolvers.Azure;
-namespace SixLabors.ImageSharp.Web.Providers.Azure
+namespace SixLabors.ImageSharp.Web.Providers.Azure;
+
+///
+/// Returns images stored in Azure Blob Storage.
+///
+public class AzureBlobStorageImageProvider : IImageProvider
{
///
- /// Returns images stored in Azure Blob Storage.
+ /// Character array to remove from paths.
+ ///
+ private static readonly char[] SlashChars = { '\\', '/' };
+
+ ///
+ /// The containers for the blob services.
+ ///
+ private readonly Dictionary containers
+ = new();
+
+ ///
+ /// Contains various helper methods based on the current configuration.
+ ///
+ private readonly FormatUtilities formatUtilities;
+
+ ///
+ /// A match function used by the resolver to identify itself as the correct resolver to use.
+ ///
+ private Func? match;
+
+ ///
+ /// Initializes a new instance of the class.
///
- public class AzureBlobStorageImageProvider : IImageProvider
+ /// The blob storage options.
+ /// Contains various format helper methods based on the current configuration.
+ /// The current service provider
+ public AzureBlobStorageImageProvider(
+ IOptions storageOptions,
+ FormatUtilities formatUtilities,
+ IServiceProvider serviceProvider)
{
- ///
- /// Character array to remove from paths.
- ///
- private static readonly char[] SlashChars = { '\\', '/' };
-
- ///
- /// The containers for the blob services.
- ///
- private readonly Dictionary containers
- = new Dictionary();
-
- ///
- /// The blob storage options.
- ///
- private readonly AzureBlobStorageImageProviderOptions storageOptions;
-
- ///
- /// Contains various helper methods based on the current configuration.
- ///
- private readonly FormatUtilities formatUtilities;
-
- ///
- /// A match function used by the resolver to identify itself as the correct resolver to use.
- ///
- private Func match;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The blob storage options.
- /// Contains various format helper methods based on the current configuration.
- public AzureBlobStorageImageProvider(
- IOptions storageOptions,
- FormatUtilities formatUtilities)
- {
- Guard.NotNull(storageOptions, nameof(storageOptions));
+ Guard.NotNull(storageOptions, nameof(storageOptions));
- this.storageOptions = storageOptions.Value;
- this.formatUtilities = formatUtilities;
+ this.formatUtilities = formatUtilities;
- foreach (AzureBlobContainerClientOptions container in this.storageOptions.BlobContainers)
- {
- this.containers.Add(
- container.ContainerName,
- new BlobContainerClient(container.ConnectionString, container.ContainerName));
- }
+ foreach (AzureBlobContainerClientOptions container in storageOptions.Value.BlobContainers)
+ {
+ BlobContainerClient client =
+ container.BlobContainerClientFactory?.Invoke(container, serviceProvider)
+ ?? new BlobContainerClient(container.ConnectionString, container.ContainerName);
+
+ this.containers.Add(client.Name, client);
}
+ }
+
+ ///
+ public ProcessingBehavior ProcessingBehavior { get; } = ProcessingBehavior.All;
+
+ ///
+ public Func Match
+ {
+ get => this.match ?? this.IsMatch;
+ set => this.match = value;
+ }
+
+ ///
+ public async Task GetAsync(HttpContext context)
+ {
+ // Strip the leading slash and container name from the HTTP request path and treat
+ // the remaining path string as the blob name.
+ // Path has already been correctly parsed before here.
+ string containerName = string.Empty;
+ BlobContainerClient? container = null;
- ///
- public ProcessingBehavior ProcessingBehavior { get; } = ProcessingBehavior.All;
+ // We want an exact match here to ensure that container names starting with
+ // the same prefix are not mixed up.
+ string? path = context.Request.Path.Value?.TrimStart(SlashChars);
- ///
- public Func Match
+ if (path is null)
{
- get => this.match ?? this.IsMatch;
- set => this.match = value;
+ return null;
}
- ///
- public async Task GetAsync(HttpContext context)
+ int index = path.IndexOfAny(SlashChars);
+ string nameToMatch = index != -1 ? path[..index] : path;
+
+ foreach (string key in this.containers.Keys)
{
- // Strip the leading slash and container name from the HTTP request path and treat
- // the remaining path string as the blob name.
- // Path has already been correctly parsed before here.
- string containerName = string.Empty;
- BlobContainerClient container = null;
-
- // We want an exact match here to ensure that container names starting with
- // the same prefix are not mixed up.
- string path = context.Request.Path.Value.TrimStart(SlashChars);
- int index = path.IndexOfAny(SlashChars);
- string nameToMatch = index != -1 ? path.Substring(0, index) : path;
-
- foreach (string key in this.containers.Keys)
+ if (nameToMatch.Equals(key, StringComparison.OrdinalIgnoreCase))
{
- if (nameToMatch.Equals(key, StringComparison.OrdinalIgnoreCase))
- {
- containerName = key;
- container = this.containers[key];
- break;
- }
+ containerName = key;
+ container = this.containers[key];
+ break;
}
+ }
- // Something has gone horribly wrong for this to happen but check anyway.
- if (container is null)
- {
- return null;
- }
+ // Something has gone horribly wrong for this to happen but check anyway.
+ if (container is null)
+ {
+ return null;
+ }
- // Blob name should be the remaining path string.
- string blobName = path.Substring(containerName.Length).TrimStart(SlashChars);
+ // Blob name should be the remaining path string.
+ string blobName = path[containerName.Length..].TrimStart(SlashChars);
- if (string.IsNullOrWhiteSpace(blobName))
- {
- return null;
- }
-
- BlobClient blob = container.GetBlobClient(blobName);
+ if (string.IsNullOrWhiteSpace(blobName))
+ {
+ return null;
+ }
- if (!await blob.ExistsAsync())
- {
- return null;
- }
+ BlobClient blob = container.GetBlobClient(blobName);
- return new AzureBlobStorageImageResolver(blob);
+ if (!await blob.ExistsAsync())
+ {
+ return null;
}
- ///
- public bool IsValidRequest(HttpContext context)
- => this.formatUtilities.GetExtensionFromUri(context.Request.GetDisplayUrl()) != null;
+ return new AzureBlobStorageImageResolver(blob);
+ }
+
+ ///
+ public bool IsValidRequest(HttpContext context)
+ => this.formatUtilities.TryGetExtensionFromUri(context.Request.GetDisplayUrl(), out _);
+
+ private bool IsMatch(HttpContext context)
+ {
+ // Only match loosely here for performance.
+ // Path matching conflicts should be dealt with by configuration.
+ string? path = context.Request.Path.Value?.TrimStart(SlashChars);
+
+ if (path is null)
+ {
+ return false;
+ }
- private bool IsMatch(HttpContext context)
+ foreach (string container in this.containers.Keys)
{
- // Only match loosly here for performance.
- // Path matching conflicts should be dealt with by configuration.
- string path = context.Request.Path.Value.TrimStart(SlashChars);
- foreach (string container in this.containers.Keys)
+ if (path.StartsWith(container, StringComparison.OrdinalIgnoreCase))
{
- if (path.StartsWith(container, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
+ return true;
}
-
- return false;
}
+
+ return false;
}
}
diff --git a/src/ImageSharp.Web.Providers.Azure/Providers/AzureBlobStorageImageProviderOptions.cs b/src/ImageSharp.Web.Providers.Azure/Providers/AzureBlobStorageImageProviderOptions.cs
index 7d4eef59..9b1b8a1b 100644
--- a/src/ImageSharp.Web.Providers.Azure/Providers/AzureBlobStorageImageProviderOptions.cs
+++ b/src/ImageSharp.Web.Providers.Azure/Providers/AzureBlobStorageImageProviderOptions.cs
@@ -1,37 +1,41 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System.Collections.Generic;
+using Azure.Storage.Blobs;
-namespace SixLabors.ImageSharp.Web.Providers.Azure
+namespace SixLabors.ImageSharp.Web.Providers.Azure;
+
+///
+/// Configuration options for the provider.
+///
+public class AzureBlobStorageImageProviderOptions
+{
+ ///
+ /// Gets or sets the collection of blob container client options.
+ ///
+ public ICollection BlobContainers { get; set; } = new HashSet();
+}
+
+///
+/// Represents a single Azure Blob Storage connection and container.
+///
+public class AzureBlobContainerClientOptions
{
///
- /// Configuration options for the provider.
+ /// Gets or sets a factory method to create an .
///
- public class AzureBlobStorageImageProviderOptions
- {
- ///
- /// Gets or sets the collection of blob container client options.
- ///
- public ICollection BlobContainers { get; set; } = new HashSet();
- }
+ public Func? BlobContainerClientFactory { get; set; }
///
- /// Represents a single Azure Blob Storage connection and container.
+ /// Gets or sets the Azure Blob Storage connection string.
+ ///
///
- public class AzureBlobContainerClientOptions
- {
- ///
- /// Gets or sets the Azure Blob Storage connection string.
- ///
- ///
- public string ConnectionString { get; set; }
+ public string ConnectionString { get; set; } = null!;
- ///
- /// Gets or sets the Azure Blob Storage container name.
- /// Must conform to Azure Blob Storage containiner naming guidlines.
- ///
- ///
- public string ContainerName { get; set; }
- }
+ ///
+ /// Gets or sets the Azure Blob Storage container name.
+ /// Must conform to Azure Blob Storage container naming guidelines.
+ ///
+ ///
+ public string ContainerName { get; set; } = null!;
}
diff --git a/src/ImageSharp.Web.Providers.Azure/Resolvers/AzureBlobStorageCacheResolver.cs b/src/ImageSharp.Web.Providers.Azure/Resolvers/AzureBlobStorageCacheResolver.cs
index e0085309..cdfad480 100644
--- a/src/ImageSharp.Web.Providers.Azure/Resolvers/AzureBlobStorageCacheResolver.cs
+++ b/src/ImageSharp.Web.Providers.Azure/Resolvers/AzureBlobStorageCacheResolver.cs
@@ -1,39 +1,35 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System.IO;
-using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using SixLabors.ImageSharp.Web.Caching.Azure;
-namespace SixLabors.ImageSharp.Web.Resolvers.Azure
+namespace SixLabors.ImageSharp.Web.Resolvers.Azure;
+
+///
+/// Provides means to manage image buffers within the .
+///
+public class AzureBlobStorageCacheResolver : IImageCacheResolver
{
+ private readonly BlobClient blob;
+
///
- /// Provides means to manage image buffers within the .
+ /// Initializes a new instance of the class.
///
- public class AzureBlobStorageCacheResolver : IImageCacheResolver
- {
- private readonly BlobClient blob;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The Azure blob.
- public AzureBlobStorageCacheResolver(BlobClient blob)
- => this.blob = blob;
+ /// The Azure blob.
+ public AzureBlobStorageCacheResolver(BlobClient blob)
+ => this.blob = blob;
- ///
- public async Task GetMetaDataAsync()
- {
- // I've had a good read through the SDK source and I believe we cannot get
- // a 304 here since 'If-Modified-Since' header is not set by default.
- BlobProperties properties = await this.blob.GetPropertiesAsync();
- return ImageCacheMetadata.FromDictionary(properties.Metadata);
- }
-
- ///
- public async Task OpenReadAsync()
- => (await this.blob.DownloadAsync()).Value.Content;
+ ///
+ public async Task GetMetaDataAsync()
+ {
+ // I've had a good read through the SDK source and I believe we cannot get
+ // a 304 here since 'If-Modified-Since' header is not set by default.
+ BlobProperties properties = await this.blob.GetPropertiesAsync();
+ return ImageCacheMetadata.FromDictionary(properties.Metadata);
}
+
+ ///
+ public Task OpenReadAsync() => this.blob.OpenReadAsync();
}
diff --git a/src/ImageSharp.Web.Providers.Azure/Resolvers/AzureBlobStorageImageResolver.cs b/src/ImageSharp.Web.Providers.Azure/Resolvers/AzureBlobStorageImageResolver.cs
index c4d74295..3b22d794 100644
--- a/src/ImageSharp.Web.Providers.Azure/Resolvers/AzureBlobStorageImageResolver.cs
+++ b/src/ImageSharp.Web.Providers.Azure/Resolvers/AzureBlobStorageImageResolver.cs
@@ -1,53 +1,48 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System;
-using System.IO;
using System.Net.Http.Headers;
-using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
-namespace SixLabors.ImageSharp.Web.Resolvers.Azure
+namespace SixLabors.ImageSharp.Web.Resolvers.Azure;
+
+///
+/// Provides means to manage image buffers within the Azure Blob file system.
+///
+public class AzureBlobStorageImageResolver : IImageResolver
{
+ private readonly BlobClient blob;
+
///
- /// Provides means to manage image buffers within the Azure Blob file system.
+ /// Initializes a new instance of the class.
///
- public class AzureBlobStorageImageResolver : IImageResolver
- {
- private readonly BlobClient blob;
+ /// The Azure blob.
+ public AzureBlobStorageImageResolver(BlobClient blob)
+ => this.blob = blob;
- ///
- /// Initializes a new instance of the class.
- ///
- /// The Azure blob.
- public AzureBlobStorageImageResolver(BlobClient blob)
- => this.blob = blob;
+ ///
+ public async Task GetMetaDataAsync()
+ {
+ // I've had a good read through the SDK source and I believe we cannot get
+ // a 304 here since 'If-Modified-Since' header is not set by default.
+ BlobProperties properties = (await this.blob.GetPropertiesAsync()).Value;
- ///
- public async Task GetMetaDataAsync()
+ // Try to parse the max age from the source. If it's not zero then we pass it along
+ // to set the cache control headers for the response.
+ TimeSpan maxAge = TimeSpan.MinValue;
+ if (CacheControlHeaderValue.TryParse(properties.CacheControl, out CacheControlHeaderValue? cacheControl))
{
- // I've had a good read through the SDK source and I believe we cannot get
- // a 304 here since 'If-Modified-Since' header is not set by default.
- BlobProperties properties = (await this.blob.GetPropertiesAsync()).Value;
-
- // Try to parse the max age from the source. If it's not zero then we pass it along
- // to set the cache control headers for the response.
- TimeSpan maxAge = TimeSpan.MinValue;
- if (CacheControlHeaderValue.TryParse(properties.CacheControl, out CacheControlHeaderValue cacheControl))
+ // Weirdly passing null to TryParse returns true.
+ if (cacheControl?.MaxAge.HasValue == true)
{
- // Weirdly passing null to TryParse returns true.
- if (cacheControl?.MaxAge.HasValue == true)
- {
- maxAge = cacheControl.MaxAge.Value;
- }
+ maxAge = cacheControl.MaxAge.Value;
}
-
- return new ImageMetadata(properties.LastModified.UtcDateTime, maxAge, properties.ContentLength);
}
- ///
- public async Task OpenReadAsync()
- => (await this.blob.DownloadAsync()).Value.Content;
+ return new ImageMetadata(properties.LastModified.UtcDateTime, maxAge, properties.ContentLength);
}
+
+ ///
+ public Task OpenReadAsync() => this.blob.OpenReadAsync();
}
diff --git a/src/ImageSharp.Web/Caching/CacheHash.cs b/src/ImageSharp.Web/Caching/CacheHash.cs
deleted file mode 100644
index ce715101..00000000
--- a/src/ImageSharp.Web/Caching/CacheHash.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-using System;
-using System.Buffers;
-using System.Runtime.CompilerServices;
-using System.Security.Cryptography;
-using System.Text;
-using Microsoft.Extensions.Options;
-using SixLabors.ImageSharp.Web.Middleware;
-
-namespace SixLabors.ImageSharp.Web.Caching
-{
- ///
- /// Creates hashed keys for the given inputs hashing them to string of length ranging from 2 to 64.
- /// Hashed keys are the result of the SHA256 computation of the input value for the given length.
- /// This ensures low collision rates with a shorter file name.
- ///
- public sealed class CacheHash : ICacheHash
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The middleware configuration options.
- public CacheHash(IOptions options)
- {
- Guard.NotNull(options, nameof(options));
- Guard.MustBeBetweenOrEqualTo(options.Value.CachedNameLength, 2, 64, nameof(options.Value.CachedNameLength));
- }
-
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public string Create(string value, uint length)
- {
- int byteCount = Encoding.ASCII.GetByteCount(value);
-
- // Allocating a buffer from the pool is ~27% slower than stackalloc so use
- // that for short strings
- if (byteCount < 257)
- {
- return HashValue(value, length, stackalloc byte[byteCount]);
- }
-
- byte[] buffer = null;
- try
- {
- buffer = ArrayPool.Shared.Rent(byteCount);
- return HashValue(value, length, buffer.AsSpan(0, byteCount));
- }
- finally
- {
- if (buffer != null)
- {
- ArrayPool.Shared.Return(buffer);
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static string HashValue(ReadOnlySpan value, uint length, Span bufferSpan)
- {
- using var hashAlgorithm = SHA256.Create();
- Encoding.ASCII.GetBytes(value, bufferSpan);
-
- // Hashed output maxes out at 32 bytes @ 256bit/8 so we're safe to use stackalloc.
- Span hash = stackalloc byte[32];
- hashAlgorithm.TryComputeHash(bufferSpan, hash, out int _);
-
- // length maxes out at 64 since we throw if options is greater.
- return HexEncoder.Encode(hash.Slice(0, (int)(length / 2)));
- }
- }
-}
diff --git a/src/ImageSharp.Web/Caching/HexEncoder.cs b/src/ImageSharp.Web/Caching/HexEncoder.cs
index 0f72a5b6..acf72787 100644
--- a/src/ImageSharp.Web/Caching/HexEncoder.cs
+++ b/src/ImageSharp.Web/Caching/HexEncoder.cs
@@ -1,60 +1,57 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System;
-using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-namespace SixLabors.ImageSharp.Web.Caching
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Provides methods for encoding byte arrays into hexidecimal strings.
+///
+internal static class HexEncoder
{
+ // LUT's that provide the hexidecimal representation of each possible byte value.
+ private static readonly char[] HexLutBase = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+ // The base LUT arranged in 16x each item order. 0 * 16, 1 * 16, .... F * 16
+ private static readonly char[] HexLutHi = Enumerable.Range(0, 256).Select(x => HexLutBase[x / 0x10]).ToArray();
+
+ // The base LUT repeated 16x.
+ private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray();
+
///
- /// Provides methods for encoding byte arrays into hexidecimal strings.
+ /// Converts a to a hexidecimal formatted padded to 2 digits.
///
- internal static class HexEncoder
+ /// The bytes.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe string Encode(ReadOnlySpan bytes)
{
- // LUT's that provide the hexidecimal representation of each possible byte value.
- private static readonly char[] HexLutBase = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
-
- // The base LUT arranged in 16x each item order. 0 * 16, 1 * 16, .... F * 16
- private static readonly char[] HexLutHi = Enumerable.Range(0, 256).Select(x => HexLutBase[x / 0x10]).ToArray();
-
- // The base LUT repeated 16x.
- private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray();
-
- ///
- /// Converts a to a hexidecimal formatted padded to 2 digits.
- ///
- /// The bytes.
- /// The .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static unsafe string Encode(ReadOnlySpan bytes)
+ fixed (byte* bytesPtr = bytes)
{
- fixed (byte* bytesPtr = bytes)
+ return string.Create(bytes.Length * 2, (Ptr: (IntPtr)bytesPtr, bytes.Length), (chars, args) =>
{
- return string.Create(bytes.Length * 2, (Ptr: (IntPtr)bytesPtr, bytes.Length), (chars, args) =>
- {
- var ros = new ReadOnlySpan((byte*)args.Ptr, args.Length);
- EncodeToUtf16(ros, chars);
- });
- }
+ var ros = new ReadOnlySpan((byte*)args.Ptr, args.Length);
+ EncodeToUtf16(ros, chars);
+ });
}
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void EncodeToUtf16(ReadOnlySpan bytes, Span chars)
- {
- ref byte bytesRef = ref MemoryMarshal.GetReference(bytes);
- ref char charRef = ref MemoryMarshal.GetReference(chars);
- ref char hiRef = ref MemoryMarshal.GetReference(HexLutHi);
- ref char lowRef = ref MemoryMarshal.GetReference(HexLutLo);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void EncodeToUtf16(ReadOnlySpan bytes, Span chars)
+ {
+ ref byte bytesRef = ref MemoryMarshal.GetReference(bytes);
+ ref char charRef = ref MemoryMarshal.GetReference(chars);
+ ref char hiRef = ref MemoryMarshal.GetReference(HexLutHi);
+ ref char lowRef = ref MemoryMarshal.GetReference(HexLutLo);
- int index = 0;
- for (int i = 0; i < bytes.Length; i++)
- {
- byte byteIndex = Unsafe.Add(ref bytesRef, i);
- Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref hiRef, byteIndex);
- Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref lowRef, byteIndex);
- }
+ int index = 0;
+ for (int i = 0; i < bytes.Length; i++)
+ {
+ byte byteIndex = Unsafe.Add(ref bytesRef, i);
+ Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref hiRef, byteIndex);
+ Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref lowRef, byteIndex);
}
}
}
diff --git a/src/ImageSharp.Web/Caching/ICacheHash.cs b/src/ImageSharp.Web/Caching/ICacheHash.cs
index 3d42246e..26324709 100644
--- a/src/ImageSharp.Web/Caching/ICacheHash.cs
+++ b/src/ImageSharp.Web/Caching/ICacheHash.cs
@@ -1,19 +1,18 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Web.Caching
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Defines a contract that allows the creation of hashed file names for storing cached images.
+///
+public interface ICacheHash
{
///
- /// Defines a contract that allows the creation of hashed file names for storing cached images.
+ /// Returns the hashed file name for the cached image file.
///
- public interface ICacheHash
- {
- ///
- /// Returns the hashed file name for the cached image file.
- ///
- /// The input value to hash.
- /// The length of the returned hash without any extensions.
- /// The .
- string Create(string value, uint length);
- }
+ /// The input value to hash.
+ /// The length of the returned hash without any extensions.
+ /// The .
+ string Create(string value, uint length);
}
diff --git a/src/ImageSharp.Web/Caching/ICacheKey.cs b/src/ImageSharp.Web/Caching/ICacheKey.cs
new file mode 100644
index 00000000..d7d3ae43
--- /dev/null
+++ b/src/ImageSharp.Web/Caching/ICacheKey.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using Microsoft.AspNetCore.Http;
+using SixLabors.ImageSharp.Web.Commands;
+
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Defines a contract that allows the creation of cache keys (used by to create hashed file names for storing cached images).
+///
+public interface ICacheKey
+{
+ ///
+ /// Creates the cache key based on the specified context and commands.
+ ///
+ /// The HTTP context.
+ /// The commands.
+ ///
+ /// The cache key.
+ ///
+ string Create(HttpContext context, CommandCollection commands);
+}
diff --git a/src/ImageSharp.Web/Caching/IImageCache.cs b/src/ImageSharp.Web/Caching/IImageCache.cs
index 20bd6e64..1ac8e5cd 100644
--- a/src/ImageSharp.Web/Caching/IImageCache.cs
+++ b/src/ImageSharp.Web/Caching/IImageCache.cs
@@ -1,32 +1,29 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System.IO;
-using System.Threading.Tasks;
using SixLabors.ImageSharp.Web.Resolvers;
// TODO: Do we add cleanup to this? Scalable caches probably shouldn't do so.
-namespace SixLabors.ImageSharp.Web.Caching
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Specifies the contract for caching images.
+///
+public interface IImageCache
{
///
- /// Specifies the contract for caching images.
+ /// Gets the image resolver associated with the specified key.
///
- public interface IImageCache
- {
- ///
- /// Gets the image resolver associated with the specified key.
- ///
- /// The cache key.
- /// The .
- Task GetAsync(string key);
+ /// The cache key.
+ /// The .
+ Task GetAsync(string key);
- ///
- /// Sets the value associated with the specified key.
- ///
- /// The cache key.
- /// The stream containing the image to store.
- /// The associated with the image to store.
- /// The task.
- Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata);
- }
+ ///
+ /// Sets the value associated with the specified key.
+ ///
+ /// The cache key.
+ /// The stream containing the image to store.
+ /// The associated with the image to store.
+ /// The task.
+ Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata);
}
diff --git a/src/ImageSharp.Web/Caching/LegacyV1CacheKey.cs b/src/ImageSharp.Web/Caching/LegacyV1CacheKey.cs
new file mode 100644
index 00000000..64b56e0b
--- /dev/null
+++ b/src/ImageSharp.Web/Caching/LegacyV1CacheKey.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Globalization;
+using System.Text;
+using Microsoft.AspNetCore.Http;
+using SixLabors.ImageSharp.Web.Commands;
+
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Maintained for compatibility purposes only this cache key implementation generates the same
+/// out as the V1 middleware. If possible, it is recommended to use the .
+///
+public class LegacyV1CacheKey : ICacheKey
+{
+ ///
+ public string Create(HttpContext context, CommandCollection commands)
+ {
+ StringBuilder sb = new(context.Request.Host.ToString());
+
+ string pathBase = context.Request.PathBase.ToString();
+ if (!string.IsNullOrWhiteSpace(pathBase))
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, "{0}/", pathBase);
+ }
+
+ string path = context.Request.Path.ToString();
+ if (!string.IsNullOrWhiteSpace(path))
+ {
+ sb.Append(path);
+ }
+
+ sb.Append(QueryString.Create(commands));
+
+ return sb.ToString().ToLowerInvariant();
+ }
+}
diff --git a/src/ImageSharp.Web/Caching/LruCache/ConcurrentTLruCache{TKey,TValue}.cs b/src/ImageSharp.Web/Caching/LruCache/ConcurrentTLruCache{TKey,TValue}.cs
index ce16a203..dbb1cf16 100644
--- a/src/ImageSharp.Web/Caching/LruCache/ConcurrentTLruCache{TKey,TValue}.cs
+++ b/src/ImageSharp.Web/Caching/LruCache/ConcurrentTLruCache{TKey,TValue}.cs
@@ -1,376 +1,372 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
-using System.Threading;
-using System.Threading.Tasks;
-namespace SixLabors.ImageSharp.Web.Caching
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Pseudo LRU implementation where LRU list is composed of 3 segments: hot, warm and cold. Cost of maintaining
+/// segments is amortized across requests. Items are only cycled when capacity is exceeded. Pure read does
+/// not cycle items if all segments are within capacity constraints.
+/// There are no global locks. On cache miss, a new item is added. Tail items in each segment are dequeued,
+/// examined, and are either enqueued or discarded.
+/// This scheme of hot, warm and cold is based on the implementation used in MemCached described online here:
+/// https://memcached.org/blog/modern-lru/
+///
+///
+/// Each segment has a capacity. When segment capacity is exceeded, items are moved as follows:
+/// 1. New items are added to hot, WasAccessed = false
+/// 2. When items are accessed, update WasAccessed = true
+/// 3. When items are moved WasAccessed is set to false.
+/// 4. When hot is full, hot tail is moved to either Warm or Cold depending on WasAccessed.
+/// 5. When warm is full, warm tail is moved to warm head or cold depending on WasAccessed.
+/// 6. When cold is full, cold tail is moved to warm head or removed from dictionary on depending on WasAccessed.
+///
+internal class ConcurrentTLruCache
+ where TKey : notnull
{
+ private readonly ConcurrentDictionary> dictionary;
+
+ private readonly ConcurrentQueue> hotQueue;
+ private readonly ConcurrentQueue> warmQueue;
+ private readonly ConcurrentQueue> coldQueue;
+
+ // Maintain count outside ConcurrentQueue, since ConcurrentQueue.Count holds a global lock
+ private int hotCount;
+ private int warmCount;
+ private int coldCount;
+
+ private readonly int hotCapacity;
+ private readonly int warmCapacity;
+ private readonly int coldCapacity;
+
+ private readonly TLruLongTicksPolicy policy;
+
///
- /// Pseudo LRU implementation where LRU list is composed of 3 segments: hot, warm and cold. Cost of maintaining
- /// segments is amortized across requests. Items are only cycled when capacity is exceeded. Pure read does
- /// not cycle items if all segments are within capacity constraints.
- /// There are no global locks. On cache miss, a new item is added. Tail items in each segment are dequeued,
- /// examined, and are either enqueued or discarded.
- /// This scheme of hot, warm and cold is based on the implementation used in MemCached described online here:
- /// https://memcached.org/blog/modern-lru/
+ /// Initializes a new instance of the class with the specified capacity and time to live that has the default.
+ /// concurrency level, and uses the default comparer for the key type.
///
- ///
- /// Each segment has a capacity. When segment capacity is exceeded, items are moved as follows:
- /// 1. New items are added to hot, WasAccessed = false
- /// 2. When items are accessed, update WasAccessed = true
- /// 3. When items are moved WasAccessed is set to false.
- /// 4. When hot is full, hot tail is moved to either Warm or Cold depending on WasAccessed.
- /// 5. When warm is full, warm tail is moved to warm head or cold depending on WasAccessed.
- /// 6. When cold is full, cold tail is moved to warm head or removed from dictionary on depending on WasAccessed.
- ///
- internal class ConcurrentTLruCache
+ /// The maximum number of elements that the FastConcurrentTLru can contain.
+ /// The time to live for cached values.
+ public ConcurrentTLruCache(int capacity, TimeSpan timeToLive)
+ : this(Environment.ProcessorCount, capacity, EqualityComparer.Default, timeToLive)
{
- private readonly ConcurrentDictionary> dictionary;
-
- private readonly ConcurrentQueue> hotQueue;
- private readonly ConcurrentQueue> warmQueue;
- private readonly ConcurrentQueue> coldQueue;
-
- // Maintain count outside ConcurrentQueue, since ConcurrentQueue.Count holds a global lock
- private int hotCount;
- private int warmCount;
- private int coldCount;
-
- private readonly int hotCapacity;
- private readonly int warmCapacity;
- private readonly int coldCapacity;
-
- private readonly TLruLongTicksPolicy policy;
-
- ///
- /// Initializes a new instance of the class with the specified capacity and time to live that has the default.
- /// concurrency level, and uses the default comparer for the key type.
- ///
- /// The maximum number of elements that the FastConcurrentTLru can contain.
- /// The time to live for cached values.
- public ConcurrentTLruCache(int capacity, TimeSpan timeToLive)
- : this(Environment.ProcessorCount, capacity, EqualityComparer.Default, timeToLive)
- {
- }
+ }
- ///
- /// Initializes a new instance of the class that has the specified concurrency level, has the.
- /// specified initial capacity, uses the specified , and has the specified time to live.
- ///
- /// The estimated number of threads that will update the ConcurrentTLru concurrently.
- /// The maximum number of elements that the ConcurrentTLru can contain.
- /// The implementation to use when comparing keys.
- /// The time to live for cached values.
- public ConcurrentTLruCache(int concurrencyLevel, int capacity, IEqualityComparer comparer, TimeSpan timeToLive)
- {
- Guard.MustBeGreaterThanOrEqualTo(capacity, 3, nameof(capacity));
- Guard.NotNull(comparer, nameof(comparer));
+ ///
+ /// Initializes a new instance of the class that has the specified concurrency level, has the.
+ /// specified initial capacity, uses the specified , and has the specified time to live.
+ ///
+ /// The estimated number of threads that will update the ConcurrentTLru concurrently.
+ /// The maximum number of elements that the ConcurrentTLru can contain.
+ /// The implementation to use when comparing keys.
+ /// The time to live for cached values.
+ public ConcurrentTLruCache(int concurrencyLevel, int capacity, IEqualityComparer comparer, TimeSpan timeToLive)
+ {
+ Guard.MustBeGreaterThanOrEqualTo(capacity, 3, nameof(capacity));
+ Guard.NotNull(comparer, nameof(comparer));
- this.hotCapacity = capacity / 3;
- this.warmCapacity = capacity / 3;
- this.coldCapacity = capacity / 3;
+ this.hotCapacity = capacity / 3;
+ this.warmCapacity = capacity / 3;
+ this.coldCapacity = capacity / 3;
- this.hotQueue = new ConcurrentQueue>();
- this.warmQueue = new ConcurrentQueue>();
- this.coldQueue = new ConcurrentQueue>();
+ this.hotQueue = new ConcurrentQueue>();
+ this.warmQueue = new ConcurrentQueue>();
+ this.coldQueue = new ConcurrentQueue>();
- int dictionaryCapacity = this.hotCapacity + this.warmCapacity + this.coldCapacity + 1;
+ int dictionaryCapacity = this.hotCapacity + this.warmCapacity + this.coldCapacity + 1;
- this.dictionary = new ConcurrentDictionary>(concurrencyLevel, dictionaryCapacity, comparer);
- this.policy = new TLruLongTicksPolicy(timeToLive);
- }
+ this.dictionary = new ConcurrentDictionary>(concurrencyLevel, dictionaryCapacity, comparer);
+ this.policy = new TLruLongTicksPolicy(timeToLive);
+ }
- // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/
- public int Count => this.dictionary.Skip(0).Count();
+ // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/
+ public int Count => this.dictionary.Skip(0).Count();
- public int HotCount => this.hotCount;
+ public int HotCount => this.hotCount;
- public int WarmCount => this.warmCount;
+ public int WarmCount => this.warmCount;
- public int ColdCount => this.coldCount;
+ public int ColdCount => this.coldCount;
- ///
- /// Attempts to get the value associated with the specified key from the cache.
- ///
- /// The key of the value to get.
- /// When this method returns, contains the object from the cache that has the specified key, or the default value of the type if the operation failed.
- /// if the key was found in the cache; otherwise, .
- public bool TryGet(TKey key, out TValue value)
+ ///
+ /// Attempts to get the value associated with the specified key from the cache.
+ ///
+ /// The key of the value to get.
+ /// When this method returns, contains the object from the cache that has the specified key, or the default value of the type if the operation failed.
+ /// if the key was found in the cache; otherwise, .
+ public bool TryGet(TKey key, [NotNullWhen(true)] out TValue? value)
+ {
+ if (this.dictionary.TryGetValue(key, out LongTickCountLruItem? item))
{
- if (this.dictionary.TryGetValue(key, out LongTickCountLruItem item))
- {
- return this.GetOrDiscard(item, out value);
- }
+ return this.GetOrDiscard(item, out value);
+ }
+ value = default;
+ return false;
+ }
+
+ // AggressiveInlining forces the JIT to inline policy.ShouldDiscard(). For LRU policy
+ // the first branch is completely eliminated due to JIT time constant propogation.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool GetOrDiscard(LongTickCountLruItem item, out TValue? value)
+ {
+ if (this.policy.ShouldDiscard(item))
+ {
+ this.Move(item, ItemDestination.Remove);
value = default;
return false;
}
- // AggressiveInlining forces the JIT to inline policy.ShouldDiscard(). For LRU policy
- // the first branch is completely eliminated due to JIT time constant propogation.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool GetOrDiscard(LongTickCountLruItem item, out TValue value)
- {
- if (this.policy.ShouldDiscard(item))
- {
- this.Move(item, ItemDestination.Remove);
- value = default;
- return false;
- }
+ value = item.Value;
+ TLruLongTicksPolicy.Touch(item);
+ return true;
+ }
- value = item.Value;
- this.policy.Touch(item);
- return true;
+ ///
+ /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
+ /// existing value if the key already exists.
+ ///
+ /// The key of the element to add.
+ /// The factory function used to generate a value for the key.
+ /// The value for the key. This will be either the existing value for the key if the key is already
+ /// in the cache, or the new value if the key was not in the dictionary.
+ public TValue GetOrAdd(TKey key, Func valueFactory)
+ {
+ if (this.TryGet(key, out TValue? value))
+ {
+ return value;
}
- ///
- /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
- /// existing value if the key already exists.
- ///
- /// The key of the element to add.
- /// The factory function used to generate a value for the key.
- /// The value for the key. This will be either the existing value for the key if the key is already
- /// in the cache, or the new value if the key was not in the dictionary.
- public TValue GetOrAdd(TKey key, Func valueFactory)
- {
- if (this.TryGet(key, out TValue value))
- {
- return value;
- }
+ // The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
+ // This is identical logic in ConcurrentDictionary.GetOrAdd method.
+ LongTickCountLruItem newItem = TLruLongTicksPolicy.CreateItem(key, valueFactory(key));
- // The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
- // This is identical logic in ConcurrentDictionary.GetOrAdd method.
- LongTickCountLruItem newItem = this.policy.CreateItem(key, valueFactory(key));
+ if (this.dictionary.TryAdd(key, newItem))
+ {
+ this.hotQueue.Enqueue(newItem);
+ Interlocked.Increment(ref this.hotCount);
+ this.Cycle();
+ return newItem.Value;
+ }
- if (this.dictionary.TryAdd(key, newItem))
- {
- this.hotQueue.Enqueue(newItem);
- Interlocked.Increment(ref this.hotCount);
- this.Cycle();
- return newItem.Value;
- }
+ return this.GetOrAdd(key, valueFactory);
+ }
- return this.GetOrAdd(key, valueFactory);
+ ///
+ /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
+ /// existing value if the key already exists.
+ ///
+ /// The key of the element to add.
+ /// The factory function used to asynchronously generate a value for the key.
+ /// A task that represents the asynchronous operation.
+ public async Task GetOrAddAsync(TKey key, Func> valueFactory)
+ {
+ if (this.TryGet(key, out TValue? value))
+ {
+ return value;
}
- ///
- /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
- /// existing value if the key already exists.
- ///
- /// The key of the element to add.
- /// The factory function used to asynchronously generate a value for the key.
- /// A task that represents the asynchronous operation.
- public async Task GetOrAddAsync(TKey key, Func> valueFactory)
+ // The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
+ // This is identical logic in ConcurrentDictionary.GetOrAdd method.
+ LongTickCountLruItem newItem = TLruLongTicksPolicy.CreateItem(key, await valueFactory(key).ConfigureAwait(false));
+
+ if (this.dictionary.TryAdd(key, newItem))
{
- if (this.TryGet(key, out TValue value))
- {
- return value;
- }
+ this.hotQueue.Enqueue(newItem);
+ Interlocked.Increment(ref this.hotCount);
+ this.Cycle();
+ return newItem.Value;
+ }
- // The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
- // This is identical logic in ConcurrentDictionary.GetOrAdd method.
- LongTickCountLruItem newItem = this.policy.CreateItem(key, await valueFactory(key).ConfigureAwait(false));
+ return await this.GetOrAddAsync(key, valueFactory).ConfigureAwait(false);
+ }
- if (this.dictionary.TryAdd(key, newItem))
+ ///
+ /// Attempts to remove and return the value that has the specified key from the cache.
+ ///
+ /// The key of the element to remove.
+ /// if the object was removed successfully; otherwise, .
+ public bool TryRemove(TKey key)
+ {
+ // Possible race condition:
+ // Thread A TryRemove(1), removes LruItem1, has reference to removed item but not yet marked as removed
+ // Thread B GetOrAdd(1) => Adds LruItem1*
+ // Thread C GetOrAdd(2), Cycle, Move(LruItem1, Removed)
+ //
+ // Thread C can run and remove LruItem1* from this.dictionary before Thread A has marked LruItem1 as removed.
+ //
+ // In this situation, a subsequent attempt to fetch 1 will be a miss. The queues will still contain LruItem1*,
+ // and it will not be marked as removed. If key 1 is fetched while LruItem1* is still in the queue, there will
+ // be two queue entries for key 1, and neither is marked as removed. Thus when LruItem1 * ages out, it will
+ // incorrectly remove 1 from the dictionary, and this cycle can repeat.
+ if (this.dictionary.TryGetValue(key, out LongTickCountLruItem? existing))
+ {
+ if (existing.WasRemoved)
{
- this.hotQueue.Enqueue(newItem);
- Interlocked.Increment(ref this.hotCount);
- this.Cycle();
- return newItem.Value;
+ return false;
}
- return await this.GetOrAddAsync(key, valueFactory).ConfigureAwait(false);
- }
-
- ///
- /// Attempts to remove and return the value that has the specified key from the cache.
- ///
- /// The key of the element to remove.
- /// if the object was removed successfully; otherwise, .
- public bool TryRemove(TKey key)
- {
- // Possible race condition:
- // Thread A TryRemove(1), removes LruItem1, has reference to removed item but not yet marked as removed
- // Thread B GetOrAdd(1) => Adds LruItem1*
- // Thread C GetOrAdd(2), Cycle, Move(LruItem1, Removed)
- //
- // Thread C can run and remove LruItem1* from this.dictionary before Thread A has marked LruItem1 as removed.
- //
- // In this situation, a subsequent attempt to fetch 1 will be a miss. The queues will still contain LruItem1*,
- // and it will not be marked as removed. If key 1 is fetched while LruItem1* is still in the queue, there will
- // be two queue entries for key 1, and neither is marked as removed. Thus when LruItem1 * ages out, it will
- // incorrectly remove 1 from the dictionary, and this cycle can repeat.
- if (this.dictionary.TryGetValue(key, out LongTickCountLruItem existing))
+ lock (existing)
{
if (existing.WasRemoved)
{
return false;
}
- lock (existing)
- {
- if (existing.WasRemoved)
- {
- return false;
- }
+ existing.WasRemoved = true;
+ }
- existing.WasRemoved = true;
- }
+ if (this.dictionary.TryRemove(key, out LongTickCountLruItem? removedItem))
+ {
+ // Mark as not accessed, it will later be cycled out of the queues because it can never be fetched
+ // from the dictionary. Note: Hot/Warm/Cold count will reflect the removed item until it is cycled
+ // from the queue.
+ removedItem.WasAccessed = false;
- if (this.dictionary.TryRemove(key, out LongTickCountLruItem removedItem))
+ if (removedItem.Value is IDisposable d)
{
- // Mark as not accessed, it will later be cycled out of the queues because it can never be fetched
- // from the dictionary. Note: Hot/Warm/Cold count will reflect the removed item until it is cycled
- // from the queue.
- removedItem.WasAccessed = false;
-
- if (removedItem.Value is IDisposable d)
- {
- d.Dispose();
- }
-
- return true;
+ d.Dispose();
}
- }
- return false;
+ return true;
+ }
}
- private void Cycle()
- {
- // There will be races when queue count == queue capacity. Two threads may each dequeue items.
- // This will prematurely free slots for the next caller. Each thread will still only cycle at most 5 items.
- // Since TryDequeue is thread safe, only 1 thread can dequeue each item. Thus counts and queue state will always
- // converge on correct over time.
- this.CycleHot();
-
- // Multi-threaded stress tests show that due to races, the warm and cold count can increase beyond capacity when
- // hit rate is very high. Double cycle results in stable count under all conditions. When contention is low,
- // secondary cycles have no effect.
- this.CycleWarm();
- this.CycleWarm();
- this.CycleCold();
- this.CycleCold();
- }
+ return false;
+ }
+
+ private void Cycle()
+ {
+ // There will be races when queue count == queue capacity. Two threads may each dequeue items.
+ // This will prematurely free slots for the next caller. Each thread will still only cycle at most 5 items.
+ // Since TryDequeue is thread safe, only 1 thread can dequeue each item. Thus counts and queue state will always
+ // converge on correct over time.
+ this.CycleHot();
+
+ // Multi-threaded stress tests show that due to races, the warm and cold count can increase beyond capacity when
+ // hit rate is very high. Double cycle results in stable count under all conditions. When contention is low,
+ // secondary cycles have no effect.
+ this.CycleWarm();
+ this.CycleWarm();
+ this.CycleCold();
+ this.CycleCold();
+ }
- private void CycleHot()
+ private void CycleHot()
+ {
+ if (this.hotCount > this.hotCapacity)
{
- if (this.hotCount > this.hotCapacity)
- {
- Interlocked.Decrement(ref this.hotCount);
+ Interlocked.Decrement(ref this.hotCount);
- if (this.hotQueue.TryDequeue(out LongTickCountLruItem item))
- {
- ItemDestination where = this.policy.RouteHot(item);
- this.Move(item, where);
- }
- else
- {
- Interlocked.Increment(ref this.hotCount);
- }
+ if (this.hotQueue.TryDequeue(out LongTickCountLruItem? item))
+ {
+ ItemDestination where = this.policy.RouteHot(item);
+ this.Move(item, where);
+ }
+ else
+ {
+ Interlocked.Increment(ref this.hotCount);
}
}
+ }
- private void CycleWarm()
+ private void CycleWarm()
+ {
+ if (this.warmCount > this.warmCapacity)
{
- if (this.warmCount > this.warmCapacity)
+ Interlocked.Decrement(ref this.warmCount);
+
+ if (this.warmQueue.TryDequeue(out LongTickCountLruItem? item))
{
- Interlocked.Decrement(ref this.warmCount);
+ ItemDestination where = this.policy.RouteWarm(item);
- if (this.warmQueue.TryDequeue(out LongTickCountLruItem item))
+ // When the warm queue is full, we allow an overflow of 1 item before redirecting warm items to cold.
+ // This only happens when hit rate is high, in which case we can consider all items relatively equal in
+ // terms of which was least recently used.
+ if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity)
{
- ItemDestination where = this.policy.RouteWarm(item);
-
- // When the warm queue is full, we allow an overflow of 1 item before redirecting warm items to cold.
- // This only happens when hit rate is high, in which case we can consider all items relatively equal in
- // terms of which was least recently used.
- if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity)
- {
- this.Move(item, where);
- }
- else
- {
- this.Move(item, ItemDestination.Cold);
- }
+ this.Move(item, where);
}
else
{
- Interlocked.Increment(ref this.warmCount);
+ this.Move(item, ItemDestination.Cold);
}
}
+ else
+ {
+ Interlocked.Increment(ref this.warmCount);
+ }
}
+ }
- private void CycleCold()
+ private void CycleCold()
+ {
+ if (this.coldCount > this.coldCapacity)
{
- if (this.coldCount > this.coldCapacity)
+ Interlocked.Decrement(ref this.coldCount);
+
+ if (this.coldQueue.TryDequeue(out LongTickCountLruItem? item))
{
- Interlocked.Decrement(ref this.coldCount);
+ ItemDestination where = this.policy.RouteCold(item);
- if (this.coldQueue.TryDequeue(out LongTickCountLruItem item))
+ if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity)
{
- ItemDestination where = this.policy.RouteCold(item);
-
- if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity)
- {
- this.Move(item, where);
- }
- else
- {
- this.Move(item, ItemDestination.Remove);
- }
+ this.Move(item, where);
}
else
{
- Interlocked.Increment(ref this.coldCount);
+ this.Move(item, ItemDestination.Remove);
}
}
+ else
+ {
+ Interlocked.Increment(ref this.coldCount);
+ }
}
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void Move(LongTickCountLruItem item, ItemDestination where)
- {
- item.WasAccessed = false;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void Move(LongTickCountLruItem item, ItemDestination where)
+ {
+ item.WasAccessed = false;
- switch (where)
- {
- case ItemDestination.Warm:
- this.warmQueue.Enqueue(item);
- Interlocked.Increment(ref this.warmCount);
- break;
- case ItemDestination.Cold:
- this.coldQueue.Enqueue(item);
- Interlocked.Increment(ref this.coldCount);
- break;
- case ItemDestination.Remove:
- if (!item.WasRemoved)
+ switch (where)
+ {
+ case ItemDestination.Warm:
+ this.warmQueue.Enqueue(item);
+ Interlocked.Increment(ref this.warmCount);
+ break;
+ case ItemDestination.Cold:
+ this.coldQueue.Enqueue(item);
+ Interlocked.Increment(ref this.coldCount);
+ break;
+ case ItemDestination.Remove:
+ if (!item.WasRemoved)
+ {
+ // Avoid race where 2 threads could remove the same key - see TryRemove for details.
+ lock (item)
{
- // Avoid race where 2 threads could remove the same key - see TryRemove for details.
- lock (item)
+ if (item.WasRemoved)
{
- if (item.WasRemoved)
- {
- break;
- }
+ break;
+ }
- if (this.dictionary.TryRemove(item.Key, out LongTickCountLruItem removedItem))
+ if (this.dictionary.TryRemove(item.Key, out LongTickCountLruItem? removedItem))
+ {
+ item.WasRemoved = true;
+ if (removedItem.Value is IDisposable d)
{
- item.WasRemoved = true;
- if (removedItem.Value is IDisposable d)
- {
- d.Dispose();
- }
+ d.Dispose();
}
}
}
+ }
- break;
- }
+ break;
}
}
}
diff --git a/src/ImageSharp.Web/Caching/LruCache/ItemDestination.cs b/src/ImageSharp.Web/Caching/LruCache/ItemDestination.cs
index d887e2e6..2275ccc8 100644
--- a/src/ImageSharp.Web/Caching/LruCache/ItemDestination.cs
+++ b/src/ImageSharp.Web/Caching/LruCache/ItemDestination.cs
@@ -1,12 +1,11 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Web.Caching
+namespace SixLabors.ImageSharp.Web.Caching;
+
+internal enum ItemDestination
{
- internal enum ItemDestination
- {
- Warm,
- Cold,
- Remove
- }
+ Warm,
+ Cold,
+ Remove
}
diff --git a/src/ImageSharp.Web/Caching/LruCache/LongTickCountLruItem{TKey,TValue}.cs b/src/ImageSharp.Web/Caching/LruCache/LongTickCountLruItem{TKey,TValue}.cs
index d0d24a15..58cb46eb 100644
--- a/src/ImageSharp.Web/Caching/LruCache/LongTickCountLruItem{TKey,TValue}.cs
+++ b/src/ImageSharp.Web/Caching/LruCache/LongTickCountLruItem{TKey,TValue}.cs
@@ -1,38 +1,37 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Web.Caching
+namespace SixLabors.ImageSharp.Web.Caching;
+
+internal class LongTickCountLruItem
{
- internal class LongTickCountLruItem
- {
- private volatile bool wasAccessed;
- private volatile bool wasRemoved;
+ private volatile bool wasAccessed;
+ private volatile bool wasRemoved;
#pragma warning disable SA1401 // Fields should be private
- public readonly TKey Key;
+ public readonly TKey Key;
- public readonly TValue Value;
+ public readonly TValue Value;
#pragma warning restore SA1401 // Fields should be private
- public LongTickCountLruItem(TKey k, TValue v, long tickCount)
- {
- this.Key = k;
- this.Value = v;
- this.TickCount = tickCount;
- }
-
- public long TickCount { get; set; }
-
- public bool WasAccessed
- {
- get => this.wasAccessed;
- set => this.wasAccessed = value;
- }
-
- public bool WasRemoved
- {
- get => this.wasRemoved;
- set => this.wasRemoved = value;
- }
+ public LongTickCountLruItem(TKey k, TValue v, long tickCount)
+ {
+ this.Key = k;
+ this.Value = v;
+ this.TickCount = tickCount;
+ }
+
+ public long TickCount { get; set; }
+
+ public bool WasAccessed
+ {
+ get => this.wasAccessed;
+ set => this.wasAccessed = value;
+ }
+
+ public bool WasRemoved
+ {
+ get => this.wasRemoved;
+ set => this.wasRemoved = value;
}
}
diff --git a/src/ImageSharp.Web/Caching/LruCache/TLruLongTicksPolicy{TKey,TValue}.cs b/src/ImageSharp.Web/Caching/LruCache/TLruLongTicksPolicy{TKey,TValue}.cs
index e63d6233..d6994ec5 100644
--- a/src/ImageSharp.Web/Caching/LruCache/TLruLongTicksPolicy{TKey,TValue}.cs
+++ b/src/ImageSharp.Web/Caching/LruCache/TLruLongTicksPolicy{TKey,TValue}.cs
@@ -1,97 +1,83 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
-namespace SixLabors.ImageSharp.Web.Caching
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Time aware Least Recently Used (TLRU) is a variant of LRU which discards the least
+/// recently used items first, and any item that has expired.
+///
+/// The type of key object.
+/// The type of value object.
+///
+/// This class measures time using stopwatch.
+///
+internal readonly struct TLruLongTicksPolicy
{
- ///
- /// Time aware Least Recently Used (TLRU) is a variant of LRU which discards the least
- /// recently used items first, and any item that has expired.
- ///
- ///
- /// This class measures time using stopwatch.
- ///
- internal readonly struct TLruLongTicksPolicy
- {
- private readonly long timeToLive;
+ private readonly long timeToLive;
- public TLruLongTicksPolicy(TimeSpan timeToLive)
- {
- this.timeToLive = timeToLive.Ticks;
- }
+ public TLruLongTicksPolicy(TimeSpan timeToLive)
+ => this.timeToLive = timeToLive.Ticks;
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public LongTickCountLruItem CreateItem(TKey key, TValue value)
- {
- return new LongTickCountLruItem(key, value, Stopwatch.GetTimestamp());
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static LongTickCountLruItem CreateItem(TKey key, TValue value)
+ => new(key, value, Stopwatch.GetTimestamp());
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void Touch(LongTickCountLruItem item)
- {
- item.WasAccessed = true;
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Touch(LongTickCountLruItem item) => item.WasAccessed = true;
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool ShouldDiscard(LongTickCountLruItem item)
- {
- if (Stopwatch.GetTimestamp() - item.TickCount > this.timeToLive)
- {
- return true;
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool ShouldDiscard(LongTickCountLruItem item)
+ => Stopwatch.GetTimestamp() - item.TickCount > this.timeToLive;
- return false;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ItemDestination RouteHot(LongTickCountLruItem item)
+ {
+ if (this.ShouldDiscard(item))
+ {
+ return ItemDestination.Remove;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public ItemDestination RouteHot(LongTickCountLruItem item)
+ if (item.WasAccessed)
{
- if (this.ShouldDiscard(item))
- {
- return ItemDestination.Remove;
- }
+ return ItemDestination.Warm;
+ }
- if (item.WasAccessed)
- {
- return ItemDestination.Warm;
- }
+ return ItemDestination.Cold;
+ }
- return ItemDestination.Cold;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ItemDestination RouteWarm(LongTickCountLruItem item)
+ {
+ if (this.ShouldDiscard(item))
+ {
+ return ItemDestination.Remove;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public ItemDestination RouteWarm(LongTickCountLruItem item)
+ if (item.WasAccessed)
{
- if (this.ShouldDiscard(item))
- {
- return ItemDestination.Remove;
- }
+ return ItemDestination.Warm;
+ }
- if (item.WasAccessed)
- {
- return ItemDestination.Warm;
- }
+ return ItemDestination.Cold;
+ }
- return ItemDestination.Cold;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ItemDestination RouteCold(LongTickCountLruItem item)
+ {
+ if (this.ShouldDiscard(item))
+ {
+ return ItemDestination.Remove;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public ItemDestination RouteCold(LongTickCountLruItem item)
+ if (item.WasAccessed)
{
- if (this.ShouldDiscard(item))
- {
- return ItemDestination.Remove;
- }
-
- if (item.WasAccessed)
- {
- return ItemDestination.Warm;
- }
-
- return ItemDestination.Remove;
+ return ItemDestination.Warm;
}
+
+ return ItemDestination.Remove;
}
}
diff --git a/src/ImageSharp.Web/Caching/PhysicalFileSystemCache.cs b/src/ImageSharp.Web/Caching/PhysicalFileSystemCache.cs
index ddc122ff..34fe94a3 100644
--- a/src/ImageSharp.Web/Caching/PhysicalFileSystemCache.cs
+++ b/src/ImageSharp.Web/Caching/PhysicalFileSystemCache.cs
@@ -1,196 +1,191 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System;
-using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
-using SixLabors.ImageSharp.Web.Middleware;
using SixLabors.ImageSharp.Web.Resolvers;
-namespace SixLabors.ImageSharp.Web.Caching
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Implements a physical file system based cache.
+///
+public class PhysicalFileSystemCache : IImageCache
{
///
- /// Implements a physical file system based cache.
+ /// The root path for the cache.
+ ///
+ private readonly string cacheRootPath;
+
+ ///
+ /// The depth of the nested cache folders structure to store the images.
+ ///
+ private readonly int cacheFolderDepth;
+
+ ///
+ /// Contains various format helper methods based on the current configuration.
///
- public class PhysicalFileSystemCache : IImageCache
+ private readonly FormatUtilities formatUtilities;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The cache configuration options.
+ /// The hosting environment the application is running in.
+ /// Contains various format helper methods based on the current configuration.
+ public PhysicalFileSystemCache(
+ IOptions options,
+ IWebHostEnvironment environment,
+ FormatUtilities formatUtilities)
{
- ///
- /// The root path for the cache.
- ///
- private readonly string cacheRootPath;
-
- ///
- /// The length of the filename to use (minus the extension) when storing images in the image cache.
- ///
- private readonly int cachedNameLength;
-
- ///
- /// The file provider abstraction.
- ///
- private readonly IFileProvider fileProvider;
-
- ///
- /// The cache configuration options.
- ///
- private readonly PhysicalFileSystemCacheOptions cacheOptions;
-
- ///
- /// The middleware configuration options.
- ///
- private readonly ImageSharpMiddlewareOptions options;
-
- ///
- /// Contains various format helper methods based on the current configuration.
- ///
- private readonly FormatUtilities formatUtilities;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The cache configuration options.
- /// The hosting environment the application is running in.
- /// The middleware configuration options.
- /// Contains various format helper methods based on the current configuration.
- public PhysicalFileSystemCache(
- IOptions cacheOptions,
-#if NETCOREAPP2_1
- IHostingEnvironment environment,
-#else
- IWebHostEnvironment environment,
-#endif
- IOptions options,
- FormatUtilities formatUtilities)
- {
- Guard.NotNull(environment, nameof(environment));
- Guard.NotNull(options, nameof(options));
- Guard.NotNullOrWhiteSpace(environment.WebRootPath, nameof(environment.WebRootPath));
-
- // Allow configuration of the cache without having to register everything.
- this.cacheOptions = cacheOptions != null ? cacheOptions.Value : new PhysicalFileSystemCacheOptions();
- this.cacheRootPath = GetCacheRoot(this.cacheOptions, environment.WebRootPath, environment.ContentRootPath);
- if (!Directory.Exists(this.cacheRootPath))
- {
- Directory.CreateDirectory(this.cacheRootPath);
- }
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(environment, nameof(environment));
- this.fileProvider = new PhysicalFileProvider(this.cacheRootPath);
- this.options = options.Value;
- this.cachedNameLength = (int)this.options.CachedNameLength;
- this.formatUtilities = formatUtilities;
- }
+ this.cacheRootPath = GetCacheRoot(options.Value, environment.WebRootPath, environment.ContentRootPath);
+ this.cacheFolderDepth = (int)options.Value.CacheFolderDepth;
+ this.formatUtilities = formatUtilities;
+ }
- ///
- /// Determine the cache root path
- ///
- /// the cache options.
- /// the webRootPath.
- /// the contentRootPath.
- /// root path.
- internal static string GetCacheRoot(PhysicalFileSystemCacheOptions cacheOptions, string webRootPath, string contentRootPath)
+ ///
+ /// Determine the cache root path
+ ///
+ /// The cache options.
+ /// The web root path.
+ /// The content root path.
+ /// representing the fully qualified cache root path.
+ /// The cache root path cannot be determined.
+ internal static string GetCacheRoot(PhysicalFileSystemCacheOptions cacheOptions, string webRootPath, string contentRootPath)
+ {
+ string cacheRootPath = cacheOptions.CacheRootPath ?? webRootPath;
+ if (string.IsNullOrEmpty(cacheRootPath))
{
- var cacheRoot = string.IsNullOrEmpty(cacheOptions.CacheRoot)
- ? webRootPath
- : cacheOptions.CacheRoot;
+ throw new InvalidOperationException("The cache root path cannot be determined, make sure it's explicitly configured or the webroot is set.");
+ }
- return Path.IsPathFullyQualified(cacheRoot)
- ? Path.Combine(cacheRoot, cacheOptions.CacheFolder)
- : Path.GetFullPath(Path.Combine(cacheRoot, cacheOptions.CacheFolder), contentRootPath);
+ if (!Path.IsPathFullyQualified(cacheRootPath))
+ {
+ // Ensure this is an absolute path (resolved to the content root path)
+ cacheRootPath = Path.GetFullPath(cacheRootPath, contentRootPath);
}
- ///
- public Task GetAsync(string key)
+ string cacheFolderPath = Path.Combine(cacheRootPath, cacheOptions.CacheFolder);
+
+ return PathUtilities.EnsureTrailingSlash(cacheFolderPath);
+ }
+
+ ///
+ public Task GetAsync(string key)
+ {
+ string path = Path.Combine(this.cacheRootPath, ToFilePath(key, this.cacheFolderDepth));
+
+ FileInfo metaFileInfo = new(ToMetaDataFilePath(path));
+ if (!metaFileInfo.Exists)
{
- string path = ToFilePath(key, this.cachedNameLength);
+ return Task.FromResult(null);
+ }
- IFileInfo metaFileInfo = this.fileProvider.GetFileInfo(this.ToMetaDataFilePath(path));
- if (!metaFileInfo.Exists)
- {
- return Task.FromResult(null);
- }
+ return Task.FromResult(new PhysicalFileSystemCacheResolver(metaFileInfo, this.formatUtilities));
+ }
- return Task.FromResult(new PhysicalFileSystemCacheResolver(metaFileInfo, this.formatUtilities));
+ ///
+ public async Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
+ {
+ string path = Path.Combine(this.cacheRootPath, ToFilePath(key, this.cacheFolderDepth));
+ string imagePath = this.ToImageFilePath(path, metadata);
+ string metaPath = ToMetaDataFilePath(path);
+ string? directory = Path.GetDirectoryName(path);
+
+ // Ensure cache directory is created before creating files
+ if (!Directory.Exists(directory) && directory is not null)
+ {
+ Directory.CreateDirectory(directory);
}
- ///
- public async Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
+ using (FileStream fileStream = File.Create(imagePath))
{
- string path = Path.Combine(this.cacheRootPath, ToFilePath(key, this.cachedNameLength));
- string imagePath = this.ToImageFilePath(path, metadata);
- string metaPath = this.ToMetaDataFilePath(path);
- string directory = Path.GetDirectoryName(path);
+ await stream.CopyToAsync(fileStream);
+ }
- if (!Directory.Exists(directory))
- {
- Directory.CreateDirectory(directory);
- }
+ using (FileStream fileStream = File.Create(metaPath))
+ {
+ await metadata.WriteAsync(fileStream);
+ }
+ }
- using (FileStream fileStream = File.Create(imagePath))
- {
- await stream.CopyToAsync(fileStream);
- }
+ ///
+ /// Gets the path to the image file based on the supplied root and metadata.
+ ///
+ /// The root path.
+ /// The image metadata.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private string ToImageFilePath(string path, in ImageCacheMetadata metaData)
+ => $"{path}.{this.formatUtilities.GetExtensionFromContentType(metaData.ContentType)}";
- using (FileStream fileStream = File.Create(metaPath))
- {
- await metadata.WriteAsync(fileStream);
- }
+ ///
+ /// Gets the path to the image file based on the supplied root.
+ ///
+ /// The root path.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static string ToMetaDataFilePath(string path) => $"{path}.meta";
+
+ ///
+ /// Converts the key into a nested file path.
+ ///
+ /// The cache key.
+ /// The depth of the nested cache folders structure to store the images.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static unsafe string ToFilePath(string key, int cacheFolderDepth)
+ {
+ if (cacheFolderDepth == 0)
+ {
+ // Short-circuit when not nesting folders
+ return key;
}
- ///
- /// Gets the path to the image file based on the supplied root and metadata.
- ///
- /// The root path.
- /// The image metadata.
- /// The .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private string ToImageFilePath(string path, in ImageCacheMetadata metaData)
- => $"{path}.{this.formatUtilities.GetExtensionFromContentType(metaData.ContentType)}";
-
- ///
- /// Gets the path to the image file based on the supplied root.
- ///
- /// The root path.
- /// The .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private string ToMetaDataFilePath(string path) => $"{path}.meta";
-
- ///
- /// Converts the key into a nested file path.
- ///
- /// The cache key.
- /// The length of the cached file name minus the extension.
- /// The .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static unsafe string ToFilePath(string key, int cachedNameLength)
+ int length;
+ int nameStartIndex;
+ if (cacheFolderDepth >= key.Length)
+ {
+ // Keep all characters in file name (legacy behavior)
+ cacheFolderDepth = key.Length;
+ length = (cacheFolderDepth * 2) + key.Length;
+ nameStartIndex = 0;
+ }
+ else
{
- // Each key substring char + separator + key
- int length = (cachedNameLength * 2) + key.Length;
- fixed (char* keyPtr = key)
+ // Remove characters used in folders from file name
+ length = cacheFolderDepth + key.Length;
+ nameStartIndex = cacheFolderDepth;
+ }
+
+ fixed (char* keyPtr = key)
+ {
+ return string.Create(length, (Ptr: (IntPtr)keyPtr, key.Length), (chars, args) =>
{
- return string.Create(length, (Ptr: (IntPtr)keyPtr, key.Length), (chars, args) =>
+ const char separator = '/';
+ ReadOnlySpan keySpan = new((char*)args.Ptr, args.Length);
+ ref char keyRef = ref MemoryMarshal.GetReference(keySpan);
+ ref char charRef = ref MemoryMarshal.GetReference(chars);
+
+ int index = 0;
+ for (int i = 0; i < cacheFolderDepth; i++)
+ {
+ Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
+ Unsafe.Add(ref charRef, index++) = separator;
+ }
+
+ for (int i = nameStartIndex; i < keySpan.Length; i++)
{
- const char separator = '/';
- var keySpan = new ReadOnlySpan((char*)args.Ptr, args.Length);
- ref char keyRef = ref MemoryMarshal.GetReference(keySpan);
- ref char charRef = ref MemoryMarshal.GetReference(chars);
-
- int index = 0;
- for (int i = 0; i < cachedNameLength; i++)
- {
- Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
- Unsafe.Add(ref charRef, index++) = separator;
- }
-
- for (int i = 0; i < keySpan.Length; i++)
- {
- Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
- }
- });
- }
+ Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
+ }
+ });
}
}
}
diff --git a/src/ImageSharp.Web/Caching/PhysicalFileSystemCacheOptions.cs b/src/ImageSharp.Web/Caching/PhysicalFileSystemCacheOptions.cs
index f796fdf1..ec2d2410 100644
--- a/src/ImageSharp.Web/Caching/PhysicalFileSystemCacheOptions.cs
+++ b/src/ImageSharp.Web/Caching/PhysicalFileSystemCacheOptions.cs
@@ -1,30 +1,34 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Web.Caching
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Configuration options for the .
+///
+public class PhysicalFileSystemCacheOptions
{
///
- /// Configuration options for the .
+ /// Gets or sets the optional cache root folder path.
+ ///
+ /// This value can be , a fully qualified absolute path,
+ /// or a path relative to the directory that contains the application
+ /// content files.
+ ///
+ ///
+ /// If not set, this will default to the directory that contains the web-servable
+ /// application content files; commonly 'wwwroot'.
+ ///
+ ///
+ public string? CacheRootPath { get; set; }
+
+ ///
+ /// Gets or sets the cache folder name.
///
- public class PhysicalFileSystemCacheOptions
- {
- ///
- /// Gets or sets the cache folder name.
- ///
- public string CacheFolder { get; set; } = "is-cache";
+ public string CacheFolder { get; set; } = "is-cache";
- ///
- /// Gets or sets the optional cache root folder.
- ///
- /// This value can be , a fully qualified absolute path,
- /// or a path relative to the directory that contains the application
- /// content files.
- ///
- ///
- /// If not set, this will default to the directory that contains the web-servable
- /// application content files; commonly 'wwwroot'.
- ///
- ///
- public string CacheRoot { get; set; }
- }
+ ///
+ /// Gets or sets the depth of the nested cache folders structure to store the images. Defaults to 8.
+ ///
+ public uint CacheFolderDepth { get; set; } = 8;
}
diff --git a/src/ImageSharp.Web/Caching/PhysicalFileSystemCacheResolver.cs b/src/ImageSharp.Web/Caching/PhysicalFileSystemCacheResolver.cs
deleted file mode 100644
index 8a10efd1..00000000
--- a/src/ImageSharp.Web/Caching/PhysicalFileSystemCacheResolver.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-using System.IO;
-using System.Threading.Tasks;
-using Microsoft.Extensions.FileProviders;
-using SixLabors.ImageSharp.Web.Caching;
-
-namespace SixLabors.ImageSharp.Web.Resolvers
-{
- ///
- /// Provides means to manage image buffers within the .
- ///
- public class PhysicalFileSystemCacheResolver : IImageCacheResolver
- {
- private readonly IFileInfo metaFileInfo;
- private readonly FormatUtilities formatUtilities;
- private ImageCacheMetadata metadata;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The cached metadata file info.
- ///
- /// Contains various format helper methods based on the current configuration.
- ///
- public PhysicalFileSystemCacheResolver(IFileInfo metaFileInfo, FormatUtilities formatUtilities)
- {
- this.metaFileInfo = metaFileInfo;
- this.formatUtilities = formatUtilities;
- }
-
- ///
- public async Task GetMetaDataAsync()
- {
- using Stream stream = this.metaFileInfo.CreateReadStream();
- this.metadata = await ImageCacheMetadata.ReadAsync(stream);
- return this.metadata;
- }
-
- ///
- public Task OpenReadAsync()
- {
- string path = Path.ChangeExtension(
- this.metaFileInfo.PhysicalPath,
- this.formatUtilities.GetExtensionFromContentType(this.metadata.ContentType));
-
- return Task.FromResult(File.OpenRead(path));
- }
- }
-}
diff --git a/src/ImageSharp.Web/Caching/SHA256CacheHash.cs b/src/ImageSharp.Web/Caching/SHA256CacheHash.cs
new file mode 100644
index 00000000..7eeae17a
--- /dev/null
+++ b/src/ImageSharp.Web/Caching/SHA256CacheHash.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.Extensions.Options;
+using SixLabors.ImageSharp.Web.Middleware;
+
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Creates hashed keys for the given inputs hashing them to string of length ranging from 2 to 64.
+/// Hashed keys are the result of the SHA256 computation of the input value for the given length.
+/// This ensures low collision rates with a shorter file name.
+///
+public sealed class SHA256CacheHash : ICacheHash
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The middleware configuration options.
+ public SHA256CacheHash(IOptions options)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.MustBeBetweenOrEqualTo(options.Value.CacheHashLength, 2, 64, nameof(options.Value.CacheHashLength));
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public string Create(string value, uint length)
+ {
+ int byteCount = Encoding.ASCII.GetByteCount(value);
+ byte[]? buffer = null;
+
+ try
+ {
+ // Allocating a buffer from the pool is ~27% slower than stackalloc so use that for short strings
+ Span bytes = byteCount <= 128
+ ? stackalloc byte[byteCount]
+ : (buffer = ArrayPool.Shared.Rent(byteCount)).AsSpan(0, byteCount);
+
+ return HashValue(value, length, bytes);
+ }
+ finally
+ {
+ if (buffer != null)
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static string HashValue(ReadOnlySpan value, uint length, Span bufferSpan)
+ {
+ Encoding.ASCII.GetBytes(value, bufferSpan);
+
+ // Hashed output maxes out at 32 bytes @ 256bit/8 so we're safe to use stackalloc
+ Span hash = stackalloc byte[32];
+ SHA256.TryHashData(bufferSpan, hash, out int _);
+
+ // Length maxes out at 64 since we throw if options is greater
+ return HexEncoder.Encode(hash[..(int)(length / 2)]);
+ }
+}
diff --git a/src/ImageSharp.Web/Caching/UriAbsoluteCacheKey.cs b/src/ImageSharp.Web/Caching/UriAbsoluteCacheKey.cs
new file mode 100644
index 00000000..ea9f126a
--- /dev/null
+++ b/src/ImageSharp.Web/Caching/UriAbsoluteCacheKey.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using Microsoft.AspNetCore.Http;
+using SixLabors.ImageSharp.Web.Commands;
+
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Creates a cache key based on the request host, path and commands.
+///
+public class UriAbsoluteCacheKey : ICacheKey
+{
+ ///
+ public string Create(HttpContext context, CommandCollection commands)
+ => CaseHandlingUriBuilder.BuildAbsolute(
+ CaseHandlingUriBuilder.CaseHandling.None,
+ context.Request.Host,
+ context.Request.PathBase,
+ context.Request.Path,
+ QueryString.Create(commands));
+}
diff --git a/src/ImageSharp.Web/Caching/UriAbsoluteLowerInvariantCacheKey.cs b/src/ImageSharp.Web/Caching/UriAbsoluteLowerInvariantCacheKey.cs
new file mode 100644
index 00000000..2e3069d4
--- /dev/null
+++ b/src/ImageSharp.Web/Caching/UriAbsoluteLowerInvariantCacheKey.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using Microsoft.AspNetCore.Http;
+using SixLabors.ImageSharp.Web.Commands;
+
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Creates a case insensitive cache key based on the request host, path and commands.
+///
+public class UriAbsoluteLowerInvariantCacheKey : ICacheKey
+{
+ ///
+ public string Create(HttpContext context, CommandCollection commands)
+ => CaseHandlingUriBuilder.BuildAbsolute(
+ CaseHandlingUriBuilder.CaseHandling.LowerInvariant,
+ context.Request.Host,
+ context.Request.PathBase,
+ context.Request.Path,
+ QueryString.Create(commands));
+}
diff --git a/src/ImageSharp.Web/Caching/UriRelativeCacheKey.cs b/src/ImageSharp.Web/Caching/UriRelativeCacheKey.cs
new file mode 100644
index 00000000..01d08740
--- /dev/null
+++ b/src/ImageSharp.Web/Caching/UriRelativeCacheKey.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using Microsoft.AspNetCore.Http;
+using SixLabors.ImageSharp.Web.Commands;
+
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Creates a cache key based on the request path and commands.
+///
+public class UriRelativeCacheKey : ICacheKey
+{
+ ///
+ public string Create(HttpContext context, CommandCollection commands)
+ => CaseHandlingUriBuilder.BuildRelative(CaseHandlingUriBuilder.CaseHandling.None, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
+}
diff --git a/src/ImageSharp.Web/Caching/UriRelativeLowerInvariantCacheKey.cs b/src/ImageSharp.Web/Caching/UriRelativeLowerInvariantCacheKey.cs
new file mode 100644
index 00000000..77e423eb
--- /dev/null
+++ b/src/ImageSharp.Web/Caching/UriRelativeLowerInvariantCacheKey.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using Microsoft.AspNetCore.Http;
+using SixLabors.ImageSharp.Web.Commands;
+
+namespace SixLabors.ImageSharp.Web.Caching;
+
+///
+/// Creates a case insensitive cache key based on the request path and commands.
+///
+public class UriRelativeLowerInvariantCacheKey : ICacheKey
+{
+ ///
+ public string Create(HttpContext context, CommandCollection commands)
+ => CaseHandlingUriBuilder.BuildRelative(CaseHandlingUriBuilder.CaseHandling.LowerInvariant, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
+}
diff --git a/src/ImageSharp.Web/CaseHandlingUriBuilder.cs b/src/ImageSharp.Web/CaseHandlingUriBuilder.cs
new file mode 100644
index 00000000..6c5b4343
--- /dev/null
+++ b/src/ImageSharp.Web/CaseHandlingUriBuilder.cs
@@ -0,0 +1,233 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using Microsoft.AspNetCore.Http;
+
+namespace SixLabors.ImageSharp.Web;
+
+///
+/// Optimized helper methods for generating encoded Uris from URI components.
+/// Much of this code has been adapted from the MIT licensed .NET runtime.
+///
+public static class CaseHandlingUriBuilder
+{
+ private static readonly Uri FallbackBaseUri = new("http://localhost/");
+ private static readonly SpanAction InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString);
+
+ ///
+ /// Provides Uri case handling options.
+ ///
+ public enum CaseHandling
+ {
+ ///
+ /// No adjustments to casing are made.
+ ///
+ None,
+
+ ///
+ /// All URI components are converted to lower case using the invariant culture before combining.
+ ///
+ LowerInvariant
+ }
+
+ ///
+ /// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
+ ///
+ /// Determines case handling for the result. is always converted to invariant lowercase.
+ /// The first portion of the request path associated with application root.
+ /// The portion of the request path that identifies the requested resource.
+ /// The query, if any.
+ /// The combined URI components, properly encoded for use in HTTP headers.
+ public static string BuildRelative(
+ CaseHandling handling,
+ PathString pathBase = default,
+ PathString path = default,
+ QueryString query = default)
+
+ // Take any potential performance hit vs concatination for code reading sanity.
+ => BuildAbsolute(handling, default, pathBase, path, query);
+
+ ///
+ /// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
+ /// Note that unicode in the HostString will be encoded as punycode and the scheme is not included
+ /// in the result.
+ ///
+ /// Determines case handling for the result. is always converted to invariant lowercase.
+ /// The host portion of the uri normally included in the Host header. This may include the port.
+ /// The first portion of the request path associated with application root.
+ /// The portion of the request path that identifies the requested resource.
+ /// The query, if any.
+ /// The combined URI components, properly encoded for use in HTTP headers.
+ public static string BuildAbsolute(
+ CaseHandling handling,
+ HostString host,
+ PathString pathBase = default,
+ PathString path = default,
+ QueryString query = default)
+ => BuildAbsolute(handling, string.Empty, host, pathBase, path, query);
+
+ ///
+ /// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
+ /// Note that unicode in the HostString will be encoded as punycode.
+ ///
+ /// Determines case handling for the result. is always converted to invariant lowercase.
+ /// http, https, etc.
+ /// The host portion of the uri normally included in the Host header. This may include the port.
+ /// The first portion of the request path associated with application root.
+ /// The portion of the request path that identifies the requested resource.
+ /// The query, if any.
+ /// The combined URI components, properly encoded for use in HTTP headers.
+ public static string BuildAbsolute(
+ CaseHandling handling,
+ string scheme,
+ HostString host,
+ PathString pathBase = default,
+ PathString path = default,
+ QueryString query = default)
+ {
+ Guard.NotNull(scheme, nameof(scheme));
+
+ string hostText = host.ToUriComponent();
+ string pathBaseText = pathBase.ToUriComponent();
+ string pathText = path.ToUriComponent();
+ string queryText = query.ToUriComponent();
+
+ // PERF: Calculate string length to allocate correct buffer size for string.Create.
+ int length =
+ (scheme.Length > 0 ? scheme.Length + Uri.SchemeDelimiter.Length : 0) +
+ hostText.Length +
+ pathBaseText.Length +
+ pathText.Length +
+ queryText.Length;
+
+ if (string.IsNullOrEmpty(pathText))
+ {
+ if (string.IsNullOrEmpty(pathBaseText))
+ {
+ pathText = "/";
+ length++;
+ }
+ }
+ else if (pathBaseText.EndsWith('/'))
+ {
+ // If the path string has a trailing slash and the other string has a leading slash, we need
+ // to trim one of them.
+ // Just decrement the total length, for now.
+ length--;
+ }
+
+ return string.Create(
+ length,
+ (handling == CaseHandling.LowerInvariant, scheme, hostText, pathBaseText, pathText, queryText),
+ InitializeAbsoluteUriStringSpanAction);
+ }
+
+ ///
+ /// Generates a string from the given absolute or relative Uri that is appropriately encoded for use in
+ /// HTTP headers. Note that a unicode host name will be encoded as punycode.
+ ///
+ /// Determines case handling for the result.
+ /// The Uri to encode.
+ /// The encoded string version of .
+ public static string Encode(CaseHandling handling, string uri)
+ {
+ Guard.NotNull(uri, nameof(uri));
+ return Encode(handling, new Uri(uri, UriKind.RelativeOrAbsolute));
+ }
+
+ ///
+ /// Generates a string from the given absolute or relative Uri that is appropriately encoded for use in
+ /// HTTP headers. Note that a unicode host name will be encoded as punycode.
+ ///
+ /// Determines case handling for the result.
+ /// The Uri to encode.
+ /// The encoded string version of .
+ public static string Encode(CaseHandling handling, Uri uri)
+ {
+ Guard.NotNull(uri, nameof(uri));
+ if (uri.IsAbsoluteUri)
+ {
+ return BuildAbsolute(
+ handling,
+ scheme: uri.Scheme,
+ host: HostString.FromUriComponent(uri),
+ pathBase: PathString.FromUriComponent(uri),
+ query: QueryString.FromUriComponent(uri));
+ }
+
+ Uri faux = new(FallbackBaseUri, uri);
+ return BuildRelative(
+ handling,
+ path: PathString.FromUriComponent(faux),
+ query: QueryString.FromUriComponent(faux));
+ }
+
+ ///
+ /// Copies the specified to the specified starting at the specified .
+ ///
+ /// The buffer to copy text to.
+ /// The buffer start index.
+ /// The text to copy.
+ /// The representing the combined text length.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int CopyTextToBuffer(Span buffer, int index, ReadOnlySpan text)
+ {
+ text.CopyTo(buffer.Slice(index, text.Length));
+ return index + text.Length;
+ }
+
+ ///
+ /// Copies the specified to the specified starting at the specified
+ /// converting each character to lowercase, using the casing rules of the invariant culture.
+ ///
+ /// The buffer to copy text to.
+ /// The buffer start index.
+ /// The text to copy.
+ /// The representing the combined text length.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int CopyTextToBufferLowerInvariant(Span buffer, int index, ReadOnlySpan text)
+ => index + text.ToLowerInvariant(buffer.Slice(index, text.Length));
+
+ ///
+ /// Initializes the URI for .
+ ///
+ /// The URI 's buffer.
+ /// The URI parts.
+ private static void InitializeAbsoluteUriString(Span buffer, (bool Lower, string Scheme, string Host, string PathBase, string Path, string Query) uriParts)
+ {
+ int index = 0;
+ ReadOnlySpan pathBaseSpan = uriParts.PathBase.AsSpan();
+
+ if (uriParts.Path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[^1] == '/')
+ {
+ // If the path string has a trailing slash and the other string has a leading slash, we need
+ // to trim one of them.
+ // Trim the last slash from pathBase. The total length was decremented before the call to string.Create.
+ pathBaseSpan = pathBaseSpan[..^1];
+ }
+
+ if (uriParts.Scheme.Length > 0)
+ {
+ index = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Scheme.AsSpan());
+ index = CopyTextToBuffer(buffer, index, Uri.SchemeDelimiter.AsSpan());
+ }
+
+ if (uriParts.Lower)
+ {
+ index = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Host.AsSpan());
+ index = CopyTextToBufferLowerInvariant(buffer, index, pathBaseSpan);
+ index = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Path.AsSpan());
+ }
+ else
+ {
+ index = CopyTextToBuffer(buffer, index, uriParts.Host.AsSpan());
+ index = CopyTextToBuffer(buffer, index, pathBaseSpan);
+ index = CopyTextToBuffer(buffer, index, uriParts.Path.AsSpan());
+ }
+
+ // Querystring is always copied as lower invariant.
+ _ = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Query.AsSpan());
+ }
+}
diff --git a/src/ImageSharp.Web/CommandHandling.cs b/src/ImageSharp.Web/CommandHandling.cs
new file mode 100644
index 00000000..2e436ea6
--- /dev/null
+++ b/src/ImageSharp.Web/CommandHandling.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Web.Commands;
+
+namespace SixLabors.ImageSharp.Web;
+
+///
+/// Provides enumeration for handling instances
+/// when processing a request.
+///
+public enum CommandHandling
+{
+ ///
+ /// The command collection will be stripped of any unknown commands.
+ ///
+ Sanitize,
+
+ ///
+ /// The command collection will be processed unaltered.
+ ///
+ None
+}
diff --git a/src/ImageSharp.Web/Commands/CommandCollection.cs b/src/ImageSharp.Web/Commands/CommandCollection.cs
new file mode 100644
index 00000000..16747d48
--- /dev/null
+++ b/src/ImageSharp.Web/Commands/CommandCollection.cs
@@ -0,0 +1,180 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Web.Commands;
+
+///
+/// Represents an ordered collection of processing commands.
+///
+public sealed class CommandCollection : KeyedCollection>
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CommandCollection()
+ : this(StringComparer.OrdinalIgnoreCase)
+ {
+ }
+
+ private CommandCollection(IEqualityComparer comparer)
+ : base(comparer)
+ {
+ }
+
+ ///
+ /// Gets an representing the keys of the collection.
+ ///
+ public IEnumerable Keys
+ {
+ get
+ {
+ foreach (KeyValuePair item in this)
+ {
+ yield return this.GetKeyForItem(item);
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the value associated with the specified key.
+ ///
+ /// The key of the value to get or set.
+ ///
+ /// The value associated with the specified key. If the specified key is not found,
+ /// a get operation throws a , and
+ /// a set operation creates a new element with the specified key.
+ ///
+ /// is null.
+ /// An element with the specified key does not exist in the collection.
+ public new string this[string key]
+ {
+ get
+ {
+ if (!this.TryGetValue(key, out string? value))
+ {
+ ThrowKeyNotFound();
+ }
+
+ return value;
+ }
+
+ set
+ {
+ if (this.TryGetValue(key, out KeyValuePair item))
+ {
+ this.SetItem(this.IndexOf(item), new(key, value));
+ }
+ else
+ {
+ this.Add(key, value);
+ }
+ }
+ }
+
+ ///
+ /// Adds an element with the provided key and value to the .
+ ///
+ /// The to use as the key of the element to add.
+ /// The to use as the value of the element to add.
+ /// is null.
+ public void Add(string key, string value) => this.Add(new(key, value));
+
+ ///
+ /// Inserts an element into the at the
+ /// specified index.
+ ///
+ /// The zero-based index at which item should be inserted.
+ /// The to use as the key of the element to insert.
+ /// The to use as the value of the element to insert.
+ /// index is less than zero. -or- index is greater than .
+ public void Insert(int index, string key, string value) => this.Insert(index, new(key, value));
+
+ ///
+ /// Gets the value associated with the specified key.
+ ///
+ /// The key whose value to get.
+ ///
+ /// When this method returns, the value associated with the specified key, if the
+ /// key is found; otherwise, the default value for the type of the value parameter.
+ /// This parameter is passed uninitialized.
+ ///
+ ///
+ /// if the object that implements contains
+ /// an element with the specified key; otherwise, .
+ ///
+ /// is null.
+ public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
+ {
+ if (this.TryGetValue(key, out KeyValuePair keyValue))
+ {
+ value = keyValue.Value;
+ return value is not null;
+ }
+
+ value = default;
+ return false;
+ }
+
+ ///
+ /// Searches for an element that matches the conditions defined by the specified
+ /// predicate, and returns the zero-based index of the first occurrence within the
+ /// entire .
+ ///
+ ///
+ /// The delegate that defines the conditions of the element to
+ /// search for.
+ ///
+ ///
+ /// The zero-based index of the first occurrence of an element that matches the conditions
+ /// defined by match, if found; otherwise, -1.
+ ///
+ /// is null.
+ public int FindIndex(Predicate match)
+ {
+ Guard.NotNull(match, nameof(match));
+
+ int index = 0;
+ foreach (KeyValuePair item in this)
+ {
+ if (match(item.Key))
+ {
+ return index;
+ }
+
+ index++;
+ }
+
+ return -1;
+ }
+
+ ///
+ /// Searches for the specified key and returns the zero-based index of the first
+ /// occurrence within the entire .
+ ///
+ /// The key to locate in the .
+ ///
+ /// The zero-based index of the first occurrence of key within the entire ,
+ /// if found; otherwise, -1.
+ ///
+ public int IndexOf(string key)
+ {
+ if (this.TryGetValue(key, out KeyValuePair item))
+ {
+ return this.IndexOf(item);
+ }
+
+ return -1;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override string GetKeyForItem(KeyValuePair item) => item.Key;
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ [DoesNotReturn]
+ private static void ThrowKeyNotFound() => throw new KeyNotFoundException();
+}
diff --git a/src/ImageSharp.Web/Commands/CommandCollectionExtensions.cs b/src/ImageSharp.Web/Commands/CommandCollectionExtensions.cs
new file mode 100644
index 00000000..126c990a
--- /dev/null
+++ b/src/ImageSharp.Web/Commands/CommandCollectionExtensions.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Web.Commands;
+
+///
+/// Extension methods for .
+///
+public static class CommandCollectionExtensions
+{
+ ///
+ /// Gets the value associated with the specified key or the default value.
+ ///
+ /// The collection instance.
+ /// The key of the value to get.
+ /// The value associated with the specified key or the default value.
+ public static string? GetValueOrDefault(this CommandCollection collection, string key)
+ {
+ collection.TryGetValue(key, out KeyValuePair result);
+ return result.Value;
+ }
+}
diff --git a/src/ImageSharp.Web/Commands/CommandParser.cs b/src/ImageSharp.Web/Commands/CommandParser.cs
index 8841b7d0..4bd0f534 100644
--- a/src/ImageSharp.Web/Commands/CommandParser.cs
+++ b/src/ImageSharp.Web/Commands/CommandParser.cs
@@ -1,82 +1,76 @@
// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
+// Licensed under the Six Labors Split License.
-using System;
-using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
-using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Web.Commands.Converters;
-namespace SixLabors.ImageSharp.Web.Commands
+namespace SixLabors.ImageSharp.Web.Commands;
+
+///
+/// Parses URI derived command values into usable commands for processors.
+///
+public sealed class CommandParser
{
+ private readonly ICommandConverter[] converters;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The collection of command converters.
+ public CommandParser(IEnumerable converters)
+ {
+ Guard.NotNull(converters, nameof(converters));
+ this.converters = converters.ToArray();
+ }
+
///
- /// Parses URI derived command values into usable commands for processors.
+ /// Parses the given string value converting it to the given type.
///
- public sealed class CommandParser
+ /// The string value to parse.
+ /// The to use as the current culture.
+ ///
+ /// The to convert the string to.
+ ///
+ /// The converted instance or the default.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public T? ParseValue(string? value, CultureInfo culture)
{
- private readonly ICommandConverter[] converters;
+ DebugGuard.NotNull(culture, nameof(culture));
+
+ Type type = typeof(T);
+ ICommandConverter? converter = Array.Find(this.converters, x => x.Type.Equals(type));
- ///
- /// Initializes a new instance of the class.
- ///
- /// The collection of command converters.
- public CommandParser(IEnumerable converters)
+ if (converter != null)
{
- Guard.NotNull(converters, nameof(converters));
- this.converters = converters.ToArray();
+ return ((ICommandConverter)converter).ConvertFrom(
+ this,
+ culture,
+ WebUtility.UrlDecode(value),
+ type);
}
- ///
- /// Parses the given string value converting it to the given type.
- ///
- /// The string value to parse.
- /// The to use as the current culture.
- ///
- /// The to convert the string to.
- ///
- /// The converted instance or the default.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public T ParseValue(string value, CultureInfo culture)
+ // This special case allows us to reuse the same converter for infinite enum types
+ // if one has not already been configured.
+ if (type.IsEnum)
{
- DebugGuard.NotNull(culture, nameof(culture));
-
- Type type = typeof(T);
- ICommandConverter converter = Array.Find(this.converters, x => x.Type.Equals(type));
-
+ converter = Array.Find(this.converters, x => x.Type.Equals(typeof(Enum)));
if (converter != null)
{
- return ((ICommandConverter)converter).ConvertFrom(
+ return (T?)((ICommandConverter