Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Release Notes

## 7.1.0
* Added `immutableCacheControl` and `mutableCacheControl` configuration options for Azure Storage and Amazon S3 feeds
* `immutableCacheControl` - Controls caching for versioned immutable files (.nupkg, .nuspec, .xml, .dll, .pdb, icons, readmes). Default: `no-store`
* `mutableCacheControl` - Controls caching for mutable feed metadata (.json, .svg). Default: `no-store`
* Both settings support standard Cache-Control header values (e.g., `public, max-age=31536000, immutable`)

## 7.0.0
* Add net10.0 support, remove netstandard2.0, net6.0 support
* Update NuGet.* packages to 7.0.1
Expand Down
8 changes: 8 additions & 0 deletions doc/client-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,14 @@ Tokens that resolve to a tokenized string will also be resolved, allowing enviro

To escape `$` use `$$`.

## Caching configuration
It is possible to configure caching header for both Azure and S3 backed feeds. This is useful when serving the feed content through a CDN. By default, caching is disabled with a value of `no-store`. You can configure caching for both mutable and immutable files. Immutable files (files which aren't supposed to change, since they are stored per version - `.nupkg`, `/readme`, `/icon`, `.nuspec`, `.xml`, `.dll`, `.pdb`) should have a long cache lifetime (unless you are making changes to live packages). Mutable files (files which are expected to change, such as `index.json` and `flatcontainer/{id}/index.json`) should have a short cache lifetime (~1 hour or whatever you are comfortable with), since it can make clients see stale feed.

| Property | Description |
|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| immutableCacheControl | Cache-Control header value for immutable versioned files (.nupkg, /readme, /icon, .nuspec, .xml, .dll, .pdb). Default is `no-store`. Example: `public, max-age=31536000, immutable` |
| mutableCacheControl | Cache-Control header value for mutable files (.json, .svg). Default is `no-store`. Example: `public, max-age=300, must-revalidate` |

## Sleet.json loading order

1. If `--config` was passed the path given will be used.
Expand Down
19 changes: 16 additions & 3 deletions src/SleetLib/FileSystem/AmazonS3File.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public class AmazonS3File : FileBase
private readonly ServerSideEncryptionMethod serverSideEncryptionMethod;
private readonly S3CannedACL? acl;
private readonly bool disablePayloadSigning;
private readonly string immutableCacheControl;
private readonly string mutableCacheControl;

internal AmazonS3File(
AmazonS3FileSystem fileSystem,
Expand All @@ -30,7 +32,9 @@ internal AmazonS3File(
ServerSideEncryptionMethod serverSideEncryptionMethod,
bool compress = true,
S3CannedACL? acl = null,
bool disablePayloadSigning = false)
bool disablePayloadSigning = false,
string? immutableCacheControl = null,
string? mutableCacheControl = null)
: base(fileSystem, rootPath, displayPath, localCacheFile, fileSystem.LocalCache.PerfTracker)
{
this.client = client;
Expand All @@ -40,6 +44,8 @@ internal AmazonS3File(
this.serverSideEncryptionMethod = serverSideEncryptionMethod;
this.acl = acl;
this.disablePayloadSigning = disablePayloadSigning;
this.immutableCacheControl = immutableCacheControl ?? "no-store";
this.mutableCacheControl = mutableCacheControl ?? "no-store";
}

protected override async Task CopyFromSource(ILogger log, CancellationToken token)
Expand Down Expand Up @@ -99,33 +105,39 @@ protected override async Task CopyToSource(ILogger log, CancellationToken token)
using (var cache = LocalCacheFile.OpenRead())
{
Stream writeStream = cache;
string? contentType = null, contentEncoding = null;
string? contentType = null, contentEncoding = null, cacheControl = "no-store";
var disposeWriteStream = false;

if (key.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase))
{
contentType = "application/zip";
cacheControl = immutableCacheControl;
}
else if (key.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)
|| key.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase))
{
contentType = "application/xml";
cacheControl = immutableCacheControl;
}
else if (key.EndsWith(".svg", StringComparison.OrdinalIgnoreCase))
{
contentType = "image/svg+xml";
cacheControl = mutableCacheControl;
}
else if (absoluteUri.AbsoluteUri.EndsWith("/icon", StringComparison.Ordinal))
{
contentType = "image/png";
cacheControl = immutableCacheControl;
}
else if (absoluteUri.AbsoluteUri.EndsWith("/readme", StringComparison.Ordinal))
{
contentType = "text/markdown";
cacheControl = immutableCacheControl;
}
else if (key.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
|| await JsonUtility.IsJsonAsync(LocalCacheFile.FullName))
{
cacheControl = mutableCacheControl;
contentType = "application/json";
if (compress && !SkipCompress())
{
Expand All @@ -137,6 +149,7 @@ protected override async Task CopyToSource(ILogger log, CancellationToken token)
else if (key.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
|| key.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase))
{
cacheControl = immutableCacheControl;
contentType = "application/octet-stream";
}
else
Expand All @@ -146,7 +159,7 @@ protected override async Task CopyToSource(ILogger log, CancellationToken token)

try
{
await UploadFileAsync(client, bucketName, key, contentType, contentEncoding, writeStream, serverSideEncryptionMethod, acl, disablePayloadSigning, token)
await UploadFileAsync(client, bucketName, key, contentType, contentEncoding, writeStream, serverSideEncryptionMethod, acl, disablePayloadSigning, cacheControl, token)
.ConfigureAwait(false);
}
finally
Expand Down
10 changes: 8 additions & 2 deletions src/SleetLib/FileSystem/AmazonS3FileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class AmazonS3FileSystem : FileSystemBase
private readonly ServerSideEncryptionMethod _serverSideEncryptionMethod;
private readonly S3CannedACL? _acl;
private readonly bool _disablePayloadSigning;
private readonly string _immutableCacheControl;
private readonly string _mutableCacheControl;

private bool? _hasBucket;

Expand All @@ -34,14 +36,18 @@ public AmazonS3FileSystem(LocalCache cache,
string? feedSubPath = null,
bool compress = true,
S3CannedACL? acl = null,
bool disablePayloadSigning = false)
bool disablePayloadSigning = false,
string? immutableCacheControl = null,
string? mutableCacheControl = null)
: base(cache, root, baseUri)
{
_client = client;
_bucketName = bucketName;
_serverSideEncryptionMethod = serverSideEncryptionMethod;
_acl = acl;
_disablePayloadSigning = disablePayloadSigning;
_immutableCacheControl = immutableCacheControl ?? "no-store";
_mutableCacheControl = mutableCacheControl ?? "no-store";

if (!string.IsNullOrEmpty(feedSubPath))
{
Expand Down Expand Up @@ -117,7 +123,7 @@ public override async Task<IReadOnlyList<ISleetFile>> GetFiles(ILogger log, Canc
private AmazonS3File CreateAmazonS3File(SleetUriPair pair)
{
var key = GetRelativePath(pair.Root);
return new AmazonS3File(this, pair.Root, pair.BaseURI, LocalCache.GetNewTempPath(), _client, _bucketName, key, _serverSideEncryptionMethod, _compress, _acl, _disablePayloadSigning);
return new AmazonS3File(this, pair.Root, pair.BaseURI, LocalCache.GetNewTempPath(), _client, _bucketName, key, _serverSideEncryptionMethod, _compress, _acl, _disablePayloadSigning, _immutableCacheControl, _mutableCacheControl);
}

public override string GetRelativePath(Uri uri)
Expand Down
3 changes: 2 additions & 1 deletion src/SleetLib/FileSystem/AmazonS3FileSystemAbstraction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public static async Task UploadFileAsync(
ServerSideEncryptionMethod serverSideEncryptionMethod,
S3CannedACL? acl,
bool disablePayloadSigning,
string? cacheControl,
CancellationToken token)
{
var transferUtility = new TransferUtility(client);
Expand All @@ -141,7 +142,7 @@ public static async Task UploadFileAsync(
InputStream = reader,
AutoCloseStream = false,
AutoResetStreamPosition = false,
Headers = { CacheControl = "no-store" },
Headers = { CacheControl = cacheControl ?? "no-store" },
DisablePayloadSigning = disablePayloadSigning
};

Expand Down
13 changes: 12 additions & 1 deletion src/SleetLib/FileSystem/AzureFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ namespace Sleet
public class AzureFile : FileBase
{
private readonly BlobClient _blob;
private readonly string _immutableCacheControl;
private readonly string _mutableCacheControl;

internal AzureFile(AzureFileSystem fileSystem, Uri rootPath, Uri displayPath, FileInfo localCacheFile, BlobClient blob)
internal AzureFile(AzureFileSystem fileSystem, Uri rootPath, Uri displayPath, FileInfo localCacheFile, BlobClient blob, string immutableCacheControl, string mutableCacheControl)
: base(fileSystem, rootPath, displayPath, localCacheFile, fileSystem.LocalCache.PerfTracker)
{
_blob = blob;
_immutableCacheControl = immutableCacheControl;
_mutableCacheControl = mutableCacheControl;
}

protected override async Task CopyFromSource(ILogger log, CancellationToken token)
Expand Down Expand Up @@ -67,20 +71,24 @@ protected override async Task CopyToSource(ILogger log, CancellationToken token)
if (_blob.Uri.AbsoluteUri.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase))
{
blobHeaders.ContentType = "application/zip";
blobHeaders.CacheControl = _immutableCacheControl;
}
else if (_blob.Uri.AbsoluteUri.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)
|| _blob.Uri.AbsoluteUri.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase))
{
blobHeaders.ContentType = "application/xml";
blobHeaders.CacheControl = _immutableCacheControl;
}
else if (_blob.Uri.AbsoluteUri.EndsWith(".svg", StringComparison.OrdinalIgnoreCase))
{
blobHeaders.ContentType = "image/svg+xml";
blobHeaders.CacheControl = _mutableCacheControl;
}
else if (_blob.Uri.AbsoluteUri.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
|| await JsonUtility.IsJsonAsync(LocalCacheFile.FullName))
{
blobHeaders.ContentType = "application/json";
blobHeaders.CacheControl = _mutableCacheControl;

if (!SkipCompress())
{
Expand All @@ -93,14 +101,17 @@ protected override async Task CopyToSource(ILogger log, CancellationToken token)
|| _blob.Uri.AbsoluteUri.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase))
{
blobHeaders.ContentType = "application/octet-stream";
blobHeaders.CacheControl = _immutableCacheControl;
}
else if (_blob.Uri.AbsoluteUri.EndsWith("/icon", StringComparison.Ordinal))
{
blobHeaders.ContentType = "image/png";
blobHeaders.CacheControl = _immutableCacheControl;
}
else if (_blob.Uri.AbsoluteUri.EndsWith("/readme", StringComparison.Ordinal))
{
blobHeaders.ContentType = "text/markdown";
blobHeaders.CacheControl = _immutableCacheControl;
}
else
{
Expand Down
10 changes: 8 additions & 2 deletions src/SleetLib/FileSystem/AzureFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ public class AzureFileSystem : FileSystemBase
public static readonly string AzureEmptyConnectionString = "DefaultEndpointsProtocol=https;AccountName=;AccountKey=;BlobEndpoint=";

private readonly BlobContainerClient _container;
private readonly string _immutableCacheControl;
private readonly string _mutableCacheControl;

public AzureFileSystem(LocalCache cache, Uri root, BlobServiceClient blobServiceClient, string container)
: this(cache, root, root, blobServiceClient, container)
{
}

public AzureFileSystem(LocalCache cache, Uri root, Uri baseUri, BlobServiceClient blobServiceClient, string container, string? feedSubPath = null)
public AzureFileSystem(LocalCache cache, Uri root, Uri baseUri, BlobServiceClient blobServiceClient, string container, string? feedSubPath = null, string? immutableCacheControl = null, string? mutableCacheControl = null)
: base(cache, root, baseUri, feedSubPath)
{
_container = blobServiceClient.GetBlobContainerClient(container);
_immutableCacheControl = immutableCacheControl ?? "no-store";
_mutableCacheControl = mutableCacheControl ?? "no-store";

var containerUri = UriUtility.EnsureTrailingSlash(_container.Uri);
var expectedPath = UriUtility.EnsureTrailingSlash(root);
Expand Down Expand Up @@ -54,7 +58,9 @@ public override ISleetFile Get(Uri path)
pair.Root,
pair.BaseURI,
LocalCache.GetNewTempPath(),
_container.GetBlobClient(GetRelativePath(path))));
_container.GetBlobClient(GetRelativePath(path)),
_immutableCacheControl,
_mutableCacheControl));
}

public override async Task<bool> Validate(ILogger log, CancellationToken token)
Expand Down
20 changes: 18 additions & 2 deletions src/SleetLib/FileSystem/FileSystemFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ public static class FileSystemFactory
{
var connectionString = JsonUtility.GetValueCaseInsensitive(sourceEntry, "connectionString");
var container = JsonUtility.GetValueCaseInsensitive(sourceEntry, "container");
var immutableCacheControlValue = JsonUtility.GetValueCaseInsensitive(sourceEntry, "immutableCacheControl");
var mutableCacheControlValue = JsonUtility.GetValueCaseInsensitive(sourceEntry, "mutableCacheControl");
var immutableCacheControl = string.IsNullOrWhiteSpace(immutableCacheControlValue) ? "no-store" : immutableCacheControlValue;
var mutableCacheControl = string.IsNullOrWhiteSpace(mutableCacheControlValue) ? "no-store" : mutableCacheControlValue;

if (string.IsNullOrEmpty(container))
{
Expand All @@ -93,7 +97,7 @@ public static class FileSystemFactory

baseUri ??= pathUri;

result = new AzureFileSystem(cache, pathUri, baseUri, blobServiceClient, container, feedSubPath);
result = new AzureFileSystem(cache, pathUri, baseUri, blobServiceClient, container, feedSubPath, immutableCacheControl, mutableCacheControl);
}
else if (type == "s3")
{
Expand All @@ -107,6 +111,16 @@ public static class FileSystemFactory
var compress = JsonUtility.GetBoolCaseInsensitive(sourceEntry, "compress", true);
var acl = JsonUtility.GetValueCaseInsensitive(sourceEntry, "acl");
var disablePayloadSigning = JsonUtility.GetBoolCaseInsensitive(sourceEntry, "disablePayloadSigning", false);
var immutableCacheControl = JsonUtility.GetValueCaseInsensitive(sourceEntry, "immutableCacheControl");
if (string.IsNullOrWhiteSpace(immutableCacheControl))
{
immutableCacheControl = "no-store";
}
var mutableCacheControl = JsonUtility.GetValueCaseInsensitive(sourceEntry, "mutableCacheControl");
if (string.IsNullOrWhiteSpace(mutableCacheControl))
{
mutableCacheControl = "no-store";
}


if (string.IsNullOrEmpty(bucketName))
Expand Down Expand Up @@ -245,7 +259,9 @@ public static class FileSystemFactory
feedSubPath,
compress,
resolvedAcl,
disablePayloadSigning
disablePayloadSigning,
immutableCacheControl,
mutableCacheControl
);
}
}
Expand Down
Loading
Loading