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; } }