Exporta archivos a DigitalOcean Spaces en .NET (C#)
Usa el AWS SDK for .NET para subir un archivo local a un Space privado de DigitalOcean; luego, descárgalo y compara su longitud y su hash SHA-256. El programa siguiente acepta archivos vacíos y archivos de hasta 64 MiB, e informa del éxito solo después de verificarlos.
Comprende la compatibilidad de DigitalOcean Spaces con S3
Spaces implementa un subconjunto de la API de S3.
Para un Space existente, configura explícitamente su endpoint regional, la región de firma y las
credenciales de Spaces. Este ejemplo usa solicitudes con estilo de ruta, por lo que el endpoint
contiene la región, pero ningún nombre de bucket: https://fra1.digitaloceanspaces.com.
Necesitas un Space privado existente y una clave de Spaces que permita escribir y leer sus objetos. Para un bucket sin política de bucket, DigitalOcean admite una clave limitada a ese bucket con permisos Read/Write/Delete. Las claves limitadas no se pueden combinar con políticas de bucket; consulta la gestión de acceso de Spaces. Un token de la API de DigitalOcean no es el par de claves de Spaces que usa este programa. Este tutorial presupone un bucket privado sin una política de lectura pública.
Instala los paquetes NuGet necesarios
Los comandos de shell usan Bash en Linux. Instala el SDK de .NET 10 actual, no solo el entorno de ejecución. Este ejemplo se probó con el SDK 10.0.402 y el entorno de ejecución 10.0.13. Las versiones de los paquetes siguientes fijan AWS SDK v4 para que la configuración sea reproducible; son versiones probadas, no versiones mínimas intrínsecamente necesarias.
Empieza en un directorio con permisos de escritura fuera de un proyecto .NET existente. El bloque
rechaza los archivos SpacesExport existentes y la configuración de directorios superiores
que podría modificar la selección del SDK, las compilaciones o la instalación de paquetes. Si la
configuración tiene éxito, la shell entra en el nuevo proyecto; si falla, permanece en su directorio
original.
(
command -v dotnet >/dev/null 2>&1 || { printf 'Install the .NET 10 SDK first.\n' >&2; exit 1; }
parent=$PWD
while :; do
for config in "$parent/global.json" "$parent"/Directory.Build.* \
"$parent/Directory.Packages.props" "$parent"/[Nn][Uu][Gg][Ee][Tt].[Cc][Oo][Nn][Ff][Ii][Gg]; do
if [ -e "$config" ]; then
printf 'Use a directory outside existing .NET configuration.\n' >&2
exit 1
fi
done
[ "$parent" = / ] && break
parent=${parent%/*}
[ -n "$parent" ] || parent=/
done
[ ! -e SpacesExport ] || { printf 'SpacesExport already exists.\n' >&2; exit 1; }
dotnet new console --framework net10.0 --name SpacesExport &&
cd SpacesExport &&
dotnet add package AWSSDK.Core --version 4.0.102.10 &&
dotnet add package AWSSDK.S3 --version 4.0.104.3
) && cd SpacesExport
Si la instalación de paquetes falla después de crear el proyecto, consérvalo. Soluciona el problema de conexión o de origen de NuGet indicado y vuelve a ejecutar estos comandos desde el directorio original:
dotnet add ./SpacesExport/SpacesExport.csproj package AWSSDK.Core --version 4.0.102.10 &&
dotnet add ./SpacesExport/SpacesExport.csproj package AWSSDK.S3 --version 4.0.104.3
Cuando el nuevo intento tenga éxito, entra en SpacesExport antes de continuar.
Usa AWS SDK for .NET con DigitalOcean Spaces
Proporciona estas variables de entorno mediante tu gestor de secretos o el entorno de la shell. La clave y el secreto deben pertenecer a Spaces, no a AWS.
| Variable | Valor |
|---|---|
SPACES_ENDPOINT | Endpoint HTTPS regional, como https://fra1.digitaloceanspaces.com |
SPACES_REGION | Región que contiene el Space existente, como fra1 |
SPACES_BUCKET | Nombre del Space existente |
SPACES_KEY_ID | ID de la clave de acceso de Spaces |
SPACES_APPLICATION_KEY | Clave secreta de Spaces |
La guía del SDK de DigitalOcean muestra
cómo seleccionar el endpoint. Pasa las claves de los objetos como cadenas sin codificar. El SDK firma
la solicitud y codifica los espacios, #, ?, +, los signos de porcentaje y los caracteres Unicode.
Reemplaza Program.cs por este programa completo. Usa archivos locales normales y
nombres válidos en UTF-8. El programa rechaza el carácter de sustitución
� en los argumentos para que un nombre de archivo nativo no válido no termine
resolviéndose silenciosamente a un archivo distinto. Mantén el archivo local sin cambios hasta que
termine la verificación y usa una clave de destino en la que ningún otro proceso esté escribiendo.
using System.Security.Cryptography;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
public static class SpacesExporter
{
private const long MaxBytes = 64L * 1024 * 1024;
public static async Task<int> Main(string[] args)
{
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(5));
Console.CancelKeyPress += (_, e) => { e.Cancel = true; deadline.Cancel(); };
try
{
if (args.Length is not (2 or 3) || (args.Length == 3 && args[2] != "--verify-only"))
throw new ArgumentException("Pass a local file, an object key, and optionally --verify-only.");
if (args.Any(value => value.Contains('\uFFFD')))
throw new ArgumentException("Use UTF-8 arguments without the replacement character.");
if (string.IsNullOrWhiteSpace(args[1]))
throw new ArgumentException("An object key is required.");
var endpoint = new Uri(Required("SPACES_ENDPOINT"));
if (endpoint.Scheme != "https" || endpoint.UserInfo.Length != 0 ||
endpoint.AbsolutePath != "/" || endpoint.Query.Length != 0 || endpoint.Fragment.Length != 0)
throw new ArgumentException("Use the HTTPS regional endpoint without a bucket or path.");
var config = new AmazonS3Config
{
ServiceURL = endpoint.AbsoluteUri,
AuthenticationRegion = Required("SPACES_REGION"),
ForcePathStyle = true,
MaxErrorRetry = 2,
RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED,
ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED
};
using var client = new AmazonS3Client(
Required("SPACES_KEY_ID"), Required("SPACES_APPLICATION_KEY"), config);
var bucket = Required("SPACES_BUCKET");
await using var input = new FileStream(args[0], FileMode.Open, FileAccess.Read, FileShare.Read);
if (input.Length > MaxBytes)
{
Console.Error.WriteLine("File exceeds the 64 MiB limit.");
return 1;
}
var expectedHash = await SHA256.HashDataAsync(input, deadline.Token);
if (args.Length == 2)
await UploadAsync(client, bucket, args[0], args[1], deadline.Token);
using var stored = await client.GetObjectAsync(bucket, args[1], deadline.Token);
if (stored.ContentLength != input.Length)
throw new InvalidDataException();
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
var buffer = new byte[64 * 1024];
long received = 0;
int count;
while ((count = await stored.ResponseStream.ReadAsync(buffer, deadline.Token)) != 0)
{
received += count;
if (received > input.Length) throw new InvalidDataException();
hash.AppendData(buffer, 0, count);
}
if (received != input.Length || !hash.GetHashAndReset().SequenceEqual(expectedHash))
throw new InvalidDataException();
Console.WriteLine($"Verified {received} bytes.");
return 0;
}
catch (OperationCanceledException) { Console.Error.WriteLine("Transfer canceled or timed out."); }
catch (AmazonS3Exception error) { Console.Error.WriteLine($"Storage returned HTTP {(int)error.StatusCode}."); }
catch (InvalidDataException) { Console.Error.WriteLine("Stored bytes do not match the local file."); }
catch (Exception) { Console.Error.WriteLine("Transfer failed. Check the file, arguments, and storage configuration."); }
return 1;
}
private static string Required(string name) =>
Environment.GetEnvironmentVariable(name) is { Length: > 0 } value
? value : throw new ArgumentException($"Missing {name}.");
public static async Task UploadAsync(IAmazonS3 client, string bucket, string path,
string key, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("An object key is required.");
await using var input = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
if (input.Length > MaxBytes) throw new IOException("File exceeds the 64 MiB limit.");
await client.PutObjectAsync(new PutObjectRequest
{
BucketName = bucket,
Key = key,
InputStream = input,
AutoCloseStream = false,
ContentType = "application/octet-stream",
UseChunkEncoding = false
}, cancellationToken);
}
}
Desde el directorio del proyecto, compila y exporta un archivo que ya tengas:
dotnet build && dotnet run --no-build -- './report.txt' 'reports/report #1.txt'
Reemplaza ambos argumentos por tu ruta local y tu clave de destino. El marcador
-- separa las opciones de la CLI de .NET de los argumentos del programa,
incluidos los nombres de archivo que comienzan por -. Una ejecución exitosa
imprime Verified seguido del número de bytes descargados y termina
con un código de salida cero. El archivo permanece en Spaces. Reutilizar la misma clave reemplaza
su contenido actual; elige una clave nueva para conservar una exportación anterior.
Añade lógica de reintentos y manejo de errores
MaxErrorRetry = 2 permite como máximo dos reintentos del SDK por solicitud para los fallos
que admiten reintentos. El SDK
acepta un token de cancelación para llamadas asíncronas.
Este programa establece un único plazo de cinco minutos para el cálculo del hash local, la subida,
los reintentos y la verificación en conjunto. Incluso un archivo de menos de 64 MiB puede superar ese
plazo con una conexión lenta. Ctrl+C solicita la cancelación mediante el mismo token. En ninguno de
los dos casos se imprime un resultado verificado, y ambos terminan con un código de salida distinto
de cero.
Una subida puede haber llegado a Spaces antes de que se pierda una respuesta o falle una verificación posterior. El programa no elimina el objeto remoto si se produce un fallo. Inspecciona esa clave y compárala con el archivo local sin modificar, sin volver a subirlo:
dotnet build && dotnet run --no-build -- './report.txt' 'reports/report #1.txt' --verify-only
Este modo solo descarga y compara; devuelve un código distinto de cero si falta el contenido o es diferente. Si la verificación tiene éxito, el objeto almacenado actualmente coincide con tu archivo. En caso contrario, resuelve el fallo antes de decidir si vuelves a subirlo. La verificación es una observación del objeto actual, no un bloqueo que impida que otro proceso escriba en él.
Transfiere en streaming archivos de tamaño limitado
La subida envía una única solicitud PUT desde un flujo de archivo abierto. La verificación lee la respuesta mediante un búfer de 64 KiB y comprueba la longitud completa y el hash, también en el caso de archivos vacíos. Descarga el archivo una vez, así que incluye esa transferencia adicional al estimar el tiempo y el ancho de banda. El límite de 64 MiB es el límite de aplicación de este ejemplo, no el límite de tamaño de los objetos de Spaces.
La configuración desactiva la codificación fragmentada de las subidas y solicita las sumas de verificación opcionales del SDK solo cuando una operación las requiere. Estos son los ajustes usados para esta forma de acceso a Spaces; la lectura posterior explícita proporciona la comprobación del contenido de extremo a extremo. El programa libera sus flujos, su respuesta y su cliente tanto si tiene éxito como si falla.
Este ejemplo no realiza subidas multiparte. Los archivos más grandes necesitan un flujo de trabajo multiparte independiente, que incluya la recuperación y la limpieza de las subidas sin terminar; aumentar esta constante no proporciona ese flujo de trabajo.
Gestiona las claves de acceso y los permisos de forma segura
Mantén las credenciales en la configuración del lado del servidor. No incluyas el secreto en una URL, un archivo de código fuente, un paquete del navegador ni un registro capturado del tráfico del SDK. Este programa proporciona el par de claves explícitamente y usa HTTPS; no hace público el Space ni cambia los permisos del bucket. Limita la salida de diagnóstico al estado y a los mensajes propios del programa.
Soluciona problemas comunes
Si aparece Storage returned HTTP 403., comprueba que proporcionaste el par de claves de Spaces, que tiene acceso tanto de lectura como de escritura a este bucket y que el endpoint y la región de firma coinciden con su ubicación. Un PUT exitoso seguido de un GET denegado también hace que falle la verificación.
Si aparece File exceeds the 64 MiB limit., usa un archivo más pequeño o una implementación multiparte independiente. Si aparece Stored bytes do not match the local file., comprueba si el archivo local o el destino cambiaron durante la operación y luego usa el modo de solo verificación con el par previsto. Una conexión rechazada o un fallo HTTP transitorio que agota los reintentos devuelve un código distinto de cero; comprueba la conectividad antes de volver a intentarlo. No consideres que una invocación fallida demuestra que no se almacenó ningún objeto.
