Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
99 changes: 99 additions & 0 deletions SW.Bitween.NativeAdapters/S3Receiver/NativeS3Receiver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Text;
using Amazon.S3;
using SW.CloudFiles.S3;
using SW.PrimitiveTypes;

namespace SW.Bitween.NativeAdapters.S3Receiver;

public class NativeS3Receiver : INativeInfolinkReceiver, IDisposable
{
private S3ReceiverInput _options = new();
private CloudFilesService? _cloudFiles;
private AmazonS3Client? _s3Client;

public Task Initialize()
{
var options = new CloudFilesOptions
{
AccessKeyId = _options.AccessKeyId,
SecretAccessKey = _options.SecretAccessKey,
ServiceUrl = _options.ServiceUrl,
BucketName = _options.BucketName,
};

_cloudFiles = new CloudFilesService(options);
_s3Client = options.CreateClient();

return Task.CompletedTask;
}

public Task Finalize()
{
_cloudFiles?.Dispose();
_s3Client?.Dispose();
return Task.CompletedTask;
}
Comment thread
hamzahalq marked this conversation as resolved.

// Safety net: the DI container disposes scoped instances when the job's scope ends,
// even if Initialize/ListFiles/GetFile/DeleteFile threw and Finalize was never reached.
public void Dispose()
{
_cloudFiles?.Dispose();
_s3Client?.Dispose();
}

public async Task<IEnumerable<string>> ListFiles()
{
var files = await _cloudFiles.ListAsync(_options.FolderName ?? string.Empty);

return files
.Where(f => !f.Key.EndsWith("/"))
.Select(f => f.Key)
.Take(_options.BatchSize)
.ToList();
}

public async Task<XchangeFile> GetFile(string fileId)
{
await using var stream = await _cloudFiles.OpenReadAsync(fileId);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
var bytes = memoryStream.ToArray();

return (_options.ResponseEncoding ?? "utf8").ToLower() switch
{
"base64" => new XchangeFile(Convert.ToBase64String(bytes), fileId),
"utf8" => new XchangeFile(Encoding.UTF8.GetString(bytes), fileId),
_ => throw new ArgumentException(
$"Unknown {nameof(S3ReceiverInput.ResponseEncoding)} '{_options.ResponseEncoding}'")
};
}

public async Task DeleteFile(string fileId)
{
if (!string.IsNullOrWhiteSpace(_options.DeleteMovesFileTo))
{
// Preserve the path relative to FolderName so files with the same name in
// different subdirectories don't collide at the destination.
var relativePath = !string.IsNullOrEmpty(_options.FolderName) && fileId.StartsWith(_options.FolderName + "/")
? fileId[(_options.FolderName.Length + 1)..]
: fileId;

// Server-side copy: S3 moves the object internally, so no bytes are
// downloaded or re-uploaded through this process.
var targetKey = $"{_options.DeleteMovesFileTo}/{relativePath}";
await _s3Client!.CopyObjectAsync(_options.BucketName, fileId, _options.BucketName, targetKey);
}

await _cloudFiles.DeleteAsync(fileId);
}
Comment thread
hamzahalq marked this conversation as resolved.

public string Name => "NativeS3Receiver";

public void InitializeStartupValues(IDictionary<string, string> settings)
{
_options = settings.ConvertTo<S3ReceiverInput>();
}

public Type StartupValuesType => typeof(S3ReceiverInput);
}
30 changes: 30 additions & 0 deletions SW.Bitween.NativeAdapters/S3Receiver/S3ReceiverInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace SW.Bitween.NativeAdapters.S3Receiver;

public class S3ReceiverInput
{
[Required]
public string AccessKeyId { get; set; } = string.Empty;

[Required]
[Secure]
public string SecretAccessKey { get; set; } = string.Empty;

[Required]
public string ServiceUrl { get; set; } = string.Empty;

[Required]
public string BucketName { get; set; } = string.Empty;

public string? FolderName { get; set; }

[DefaultValue(50)]
public int BatchSize { get; set; } = 50;

[DefaultValue("utf8")]
public string ResponseEncoding { get; set; } = "utf8";

public string? DeleteMovesFileTo { get; set; }
}
48 changes: 48 additions & 0 deletions SW.Bitween.NativeAdapters/S3UploadHandler/NativeS3UploadHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using SW.CloudFiles.S3;
using SW.PrimitiveTypes;

namespace SW.Bitween.NativeAdapters.S3UploadHandler;

public class NativeS3UploadHandler : INativeInfolinkHandler
{
private S3UploadHandlerInput _options = new();

public async Task<XchangeFile> Handle(XchangeFile xchangeFile)
{
using var cloudFiles = new CloudFilesService(new CloudFilesOptions
{
AccessKeyId = _options.AccessKeyId,
SecretAccessKey = _options.SecretAccessKey,
ServiceUrl = _options.ServiceUrl,
BucketName = _options.BucketName,
});

var key = _options.FileName;
if (string.IsNullOrWhiteSpace(key))
{
var extension = _options.FileExtension?.TrimStart('.');
var name = $"{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}";
key = string.IsNullOrEmpty(extension) ? name : $"{name}.{extension}";

if (!string.IsNullOrWhiteSpace(_options.FolderName))
key = $"{_options.FolderName}/{key}";
}
Comment thread
hamzahalq marked this conversation as resolved.

await cloudFiles.WriteTextAsync(xchangeFile.Data, new WriteFileSettings
{
Key = key,
ContentType = _options.ContentType,
});

return new XchangeFile(key, xchangeFile.Filename);
}

public string Name => "NativeS3UploadHandler";

public void InitializeStartupValues(IDictionary<string, string> settings)
{
_options = settings.ConvertTo<S3UploadHandlerInput>();
}

public Type StartupValuesType => typeof(S3UploadHandlerInput);
}
29 changes: 29 additions & 0 deletions SW.Bitween.NativeAdapters/S3UploadHandler/S3UploadHandlerInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace SW.Bitween.NativeAdapters.S3UploadHandler;

public class S3UploadHandlerInput
{
[Required]
public string AccessKeyId { get; set; } = string.Empty;

[Required]
[Secure]
public string SecretAccessKey { get; set; } = string.Empty;

[Required]
public string ServiceUrl { get; set; } = string.Empty;

[Required]
public string BucketName { get; set; } = string.Empty;

public string? FolderName { get; set; }

public string? FileName { get; set; }

public string? FileExtension { get; set; }

[DefaultValue("text/plain")]
public string ContentType { get; set; } = "text/plain";
}
1 change: 1 addition & 0 deletions SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<PackageReference Include="Rebex.Pop3" Version="8.0.9673" />
<PackageReference Include="Rebex.Sftp" Version="8.0.9673" />
<PackageReference Include="Scriban" Version="7.0.6" />
<PackageReference Include="SimplyWorks.CloudFiles.S3" Version="8.1.1" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check NuGet for the latest version and any security advisories.
curl -s "https://api.nuget.org/v3-flatcontainer/simplyworks.cloudfiles.s3/index.json" | jq '.versions[-3:]'
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: NUGET, package: "SimplyWorks.CloudFiles.S3") {
    nodes {
      advisory { summary severity publishedAt }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}'

Repository: simplify9/Bitween-api

Length of output: 244


Upgrade SimplyWorks.CloudFiles.S3 to 8.1.6 8.1.1 is several patch releases behind the latest available release.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj` at line 25, The
package reference for SimplyWorks.CloudFiles.S3 is outdated and should be
updated to the requested patch release. Change the Version on the
PackageReference in the SW.Bitween.NativeAdapters project file from the current
8.1.1 to 8.1.6, keeping the package name unchanged and ensuring the project
still restores cleanly after the version bump.

<PackageReference Include="SimplyWorks.PrimitiveTypes" Version="8.1.2" />
</ItemGroup>

Expand Down
8 changes: 8 additions & 0 deletions SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using SW.Bitween.NativeAdapters.RebexFtpReceiver;
using SW.Bitween.NativeAdapters.RebexFtpUploadHandler;
using SW.Bitween.NativeAdapters.RebexPop3Receiver;
using SW.Bitween.NativeAdapters.S3Receiver;
using SW.Bitween.NativeAdapters.S3UploadHandler;

namespace SW.Bitween.NativeAdapters;

Expand Down Expand Up @@ -38,6 +40,12 @@ public static void AddNativeAdapters(this IServiceCollection serviceCollection,
serviceCollection.AddScoped<INativeInfolinkReceiver, NativePop3Receiver>();
serviceCollection.AddScoped<INativeAdapter, NativePop3Receiver>();

serviceCollection.AddScoped<INativeInfolinkHandler, NativeS3UploadHandler>();
serviceCollection.AddScoped<INativeAdapter, NativeS3UploadHandler>();

serviceCollection.AddScoped<INativeInfolinkReceiver, NativeS3Receiver>();
serviceCollection.AddScoped<INativeAdapter, NativeS3Receiver>();

if (!string.IsNullOrEmpty(rebexLicenseKey))
{
serviceCollection.AddScoped<INativeInfolinkReceiver>(_ => new NativeRebexPop3Receiver(rebexLicenseKey));
Expand Down