using System.Diagnostics.CodeAnalysis;
namespace DodoSSH.Client.Domain;
///
/// An S3-compatible bucket and the credentials that reach it, decrypted.
///
///
///
/// Named for the protocol rather than for Amazon, because everything here works the same against MinIO, R2,
/// Backblaze or Ceph — and for those, is an address on somebody's own network. The
/// interface says S3, which is what people call the protocol; the type says what it is.
///
///
/// is a password, and everything this codebase does about passwords applies
/// to it. It is inside the encrypted payload, it never appears in a log line — the activity log records
/// the field's name and not its value — and the merge reports that it differed rather than what it was.
///
///
public sealed record ObjectStoreSecret : IVaultSecret
{
/// What the user calls this bucket. The only name it has anywhere.
public required string Label { get; init; }
/// The bucket.
public required string Bucket { get; init; }
/// The access key id.
public required string AccessKeyId { get; init; }
/// The secret access key.
public required string SecretAccessKey { get; init; }
///
/// The region, or null to let the endpoint decide.
///
///
/// Required by AWS and ignored by several S3-compatible services, which is why it is nullable rather than
/// defaulted to us-east-1. A default would be a guess presented as configuration, and the guess is
/// wrong for exactly the self-hosted case this field exists to support.
///
public string? Region { get; init; }
///
/// The service endpoint, or null for Amazon's own.
///
///
/// Null means AWS and the SDK resolves the host from . Anything else is a URL, and it
/// is the field that makes this work against a MinIO in a cupboard.
///
public string? Endpoint { get; init; }
///
/// Whether to address the bucket as a path rather than as a subdomain.
///
///
/// https://endpoint/bucket/key instead of https://bucket.endpoint/key. Off for AWS, on for
/// nearly every self-hosted service — MinIO in its default configuration has no wildcard DNS, so
/// virtual-host addressing simply does not resolve. It is a setting rather than a guess because getting
/// it wrong produces a name-resolution failure that says nothing about buckets.
///
public bool UsePathStyle { get; init; }
/// Free-text notes.
public string? Notes { get; init; }
/// Whether this is storable, and why not if it is not.
///
/// The endpoint is checked for being a well-formed absolute URL when it is set at all. A relative one, or
/// a bare hostname, produces an SDK failure at the first request whose message names neither the field
/// nor this bucket — and the person reading it has typically just typed the value.
///
public bool TryValidate([NotNullWhen(false)] out string? reason)
{
if (string.IsNullOrWhiteSpace(Label))
{
reason = "A bucket needs a name.";
return false;
}
if (string.IsNullOrWhiteSpace(Bucket))
{
reason = "A bucket needs the bucket it points at.";
return false;
}
if (string.IsNullOrWhiteSpace(AccessKeyId) || string.IsNullOrWhiteSpace(SecretAccessKey))
{
reason = "A bucket needs an access key id and a secret access key.";
return false;
}
if (Endpoint is not null)
{
if (!Uri.TryCreate(Endpoint, UriKind.Absolute, out var endpoint))
{
reason = "The endpoint has to be a full URL, like https://minio.internal:9000.";
return false;
}
if (!string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal)
&& !string.Equals(endpoint.Scheme, Uri.UriSchemeHttp, StringComparison.Ordinal))
{
reason = "The endpoint has to be http or https.";
return false;
}
}
if (Region is null && Endpoint is null)
{
// With neither, the SDK has nothing to resolve a host from and fails at the first request with
// a message about a missing region rather than about this bucket.
reason = "A bucket needs a region, an endpoint, or both.";
return false;
}
reason = null;
return true;
}
}