Saltar al contenido principal

Metadatos

Los metadatos permiten asociar información adicional a cada archivo almacenado: tipo de contenido, fechas, etiquetas, información de autoría, estado de flujos de trabajo y cualquier par clave-valor que necesite tu aplicación.

Métodos disponibles

// Obtener todos los metadatos de un archivo
Task<StorageResult<FileMetadata>> GetMetadataAsync(
string path,
CancellationToken ct = default);

// Reemplazar los metadatos personalizados de un archivo existente
Task<StorageResult> SetMetadataAsync(
string path,
Dictionary<string, string> metadata,
CancellationToken ct = default);

FileMetadata — todos los campos

public class FileMetadata
{
/// <summary>Ruta del archivo en el almacenamiento.</summary>
public required string Path { get; init; }

/// <summary>Tamaño del archivo almacenado en bytes (puede diferir del original si está comprimido).</summary>
public long SizeBytes { get; init; }

/// <summary>Tipo MIME del archivo.</summary>
public string? ContentType { get; init; }

/// <summary>Fecha y hora de creación del archivo.</summary>
public DateTimeOffset? CreatedAt { get; init; }

/// <summary>Fecha y hora de la última modificación.</summary>
public DateTimeOffset? LastModified { get; init; }

/// <summary>Hash ETag del archivo (formato depende del proveedor).</summary>
public string? ETag { get; init; }

/// <summary>Metadatos personalizados definidos al subir o con SetMetadataAsync.</summary>
public IReadOnlyDictionary<string, string> CustomMetadata { get; init; }
= new Dictionary<string, string>();

/// <summary>Etiquetas del archivo para búsqueda y clasificación.</summary>
public IReadOnlyList<string> Tags { get; init; } = [];

/// <summary>true si el archivo fue cifrado con EncryptionMiddleware.</summary>
public bool IsEncrypted { get; init; }

/// <summary>true si el archivo fue comprimido con CompressionMiddleware.</summary>
public bool IsCompressed { get; init; }

/// <summary>Hash SHA-256 del contenido original (disponible si se usó deduplicación).</summary>
public string? ContentHash { get; init; }
}

Descripción de campos

CampoTipoDescripción
PathstringRuta completa del archivo
SizeByteslongTamaño en bytes del archivo almacenado
ContentTypestring?Tipo MIME. Ej: image/jpeg, application/pdf
CreatedAtDateTimeOffset?Fecha de creación (no todos los proveedores la devuelven)
LastModifiedDateTimeOffset?Fecha de última modificación
ETagstring?Hash de integridad según el proveedor
CustomMetadataIReadOnlyDictionary<string,string>Metadatos personalizados
TagsIReadOnlyList<string>Etiquetas para clasificación
IsEncryptedbooltrue si fue cifrado con EncryptionMiddleware
IsCompressedbooltrue si fue comprimido con CompressionMiddleware
ContentHashstring?SHA-256 del contenido original

GetMetadataAsync

Uso básico

var resultado = await storage.GetMetadataAsync("documentos/contrato.pdf", ct);

if (resultado.IsSuccess)
{
var meta = resultado.Value!;
Console.WriteLine($"Archivo: {meta.Path}");
Console.WriteLine($"Tamaño: {meta.SizeBytes / 1024.0:F2} KB");
Console.WriteLine($"Tipo MIME: {meta.ContentType}");
Console.WriteLine($"Creado: {meta.CreatedAt:R}");
Console.WriteLine($"Modificado: {meta.LastModified:R}");
Console.WriteLine($"Cifrado: {meta.IsEncrypted}");
Console.WriteLine($"Comprimido: {meta.IsCompressed}");

foreach (var (clave, valor) in meta.CustomMetadata)
Console.WriteLine($" {clave}: {valor}");
}

En un endpoint de información de archivo

app.MapGet("/api/archivos/{*ruta}/info", async (
string ruta,
IStorageProvider storage,
CancellationToken ct) =>
{
var resultado = await storage.GetMetadataAsync(Uri.UnescapeDataString(ruta), ct);

if (!resultado.IsSuccess)
{
return resultado.ErrorCode == StorageErrorCode.NotFound
? Results.NotFound()
: Results.StatusCode(500);
}

var meta = resultado.Value!;
return Results.Ok(new
{
ruta = meta.Path,
tamanoBytes = meta.SizeBytes,
tamanoMb = Math.Round(meta.SizeBytes / 1024.0 / 1024.0, 2),
tipoContenido = meta.ContentType,
creadoEn = meta.CreatedAt,
modificadoEn = meta.LastModified,
estaCifrado = meta.IsEncrypted,
estaComprimido = meta.IsCompressed,
etiquetas = meta.Tags,
metadatos = meta.CustomMetadata
});
});

SetMetadataAsync

Reemplaza todos los metadatos personalizados del archivo. No es una operación de merge parcial.

Uso básico

var resultado = await storage.SetMetadataAsync(
path: "contratos/contrato-001.pdf",
metadata: new Dictionary<string, string>
{
["estado"] = "firmado",
["firmado-por"] = "Ana García",
["fecha-firma"] = DateTimeOffset.UtcNow.ToString("O"),
["version"] = "final",
["revisado-por-juridico"] = "true"
},
ct);

if (!resultado.IsSuccess)
logger.LogError("Error al actualizar metadatos: {Mensaje}", resultado.ErrorMessage);

Actualización parcial (merge manual)

Como SetMetadataAsync reemplaza todos los metadatos, implementa el merge manualmente cuando solo necesitas actualizar algunos campos:

public async Task ActualizarMetadatosParciales(
IStorageProvider storage,
string ruta,
Dictionary<string, string> nuevosCampos,
CancellationToken ct)
{
var metaActual = await storage.GetMetadataAsync(ruta, ct);
if (!metaActual.IsSuccess) return;

// Merge: combinar los metadatos existentes con los nuevos
var metadatos = metaActual.Value!.CustomMetadata
.ToDictionary(kv => kv.Key, kv => kv.Value);

foreach (var (clave, valor) in nuevosCampos)
metadatos[clave] = valor;

await storage.SetMetadataAsync(ruta, metadatos, ct);
}

Soporte por proveedor

ProveedorMetadatos personalizadosLímite por claveLímite total
Amazon S32 KB por par2 KB total
Azure Blob Storage8 KB totalSin límite fijo
Google Cloud StorageSin límite documentadoSin límite fijo
OCI Object StorageSin límite documentadoSin límite fijo
Supabase StorageLimitado por la BDSin límite fijo
Sistema de archivos localSin límite (JSON)Sin límite
Advertencia

Amazon S3 tiene un límite estricto de 2 KB totales para todos los metadatos de un objeto. Si necesitas almacenar más información, considera guardar los metadatos extendidos en una base de datos usando la ruta del archivo como clave primaria, o serializa los datos en un único campo JSON comprimido.

Casos de uso prácticos

Sistema de auditoría de acceso

// Al subir: registrar quién subió el archivo
await storage.UploadAsync(new UploadRequest
{
Path = ruta,
Content = stream,
Metadata = new Dictionary<string, string>
{
["creado-por-id"] = usuario.Id.ToString(),
["creado-por-nombre"] = usuario.NombreCompleto,
["ip-origen"] = httpContext.Connection.RemoteIpAddress?.ToString() ?? "desconocida",
["timestamp-utc"] = DateTimeOffset.UtcNow.ToString("O"),
["sesion-id"] = httpContext.Session.Id,
["origen-app"] = "portal-web"
}
}, ct);

Flujo de aprobación de documentos

public enum EstadoDocumento
{
Borrador,
EnRevision,
Aprobado,
Rechazado
}

public async Task CambiarEstadoDocumento(
IStorageProvider storage,
string ruta,
EstadoDocumento nuevoEstado,
string revisorId,
string? comentario,
CancellationToken ct)
{
var meta = await storage.GetMetadataAsync(ruta, ct);
if (!meta.IsSuccess) return;

var metadatos = meta.Value!.CustomMetadata
.ToDictionary(kv => kv.Key, kv => kv.Value);

metadatos["estado"] = nuevoEstado.ToString();
metadatos["revisor-id"] = revisorId;
metadatos["fecha-revision"] = DateTimeOffset.UtcNow.ToString("O");
if (comentario is not null)
metadatos["comentario-revision"] = comentario;

await storage.SetMetadataAsync(ruta, metadatos, ct);
}

Versionado de archivos

// Al crear una nueva versión, actualizar los metadatos de la anterior
await storage.SetMetadataAsync(rutaVersionAnterior, new Dictionary<string, string>
{
["version-numero"] = "2",
["version-siguiente"] = rutaNuevaVersion,
["archivado-en"] = DateTimeOffset.UtcNow.ToString("O"),
["archivado-por"] = usuarioId
}, ct);

// Al subir la nueva versión
await storage.UploadAsync(new UploadRequest
{
Path = rutaNuevaVersion,
Content = contenidoNuevoStream,
Metadata = new Dictionary<string, string>
{
["version-numero"] = "3",
["version-anterior"] = rutaVersionAnterior,
["motivo-cambio"] = "Actualización de cláusulas 5 y 7"
}
}, ct);

Verificar tipo de contenido antes de procesar

public async Task<bool> EsImagenValida(
IStorageProvider storage,
string ruta,
CancellationToken ct)
{
var meta = await storage.GetMetadataAsync(ruta, ct);
if (!meta.IsSuccess) return false;

var tiposPermitidos = new HashSet<string>
{
"image/jpeg", "image/png", "image/webp", "image/avif"
};

return meta.Value!.ContentType is not null
&& tiposPermitidos.Contains(meta.Value.ContentType);
}
Consejo

Usa prefijos coherentes en las claves de metadatos para evitar colisiones con claves del sistema. Por ejemplo, usa un prefijo específico de tu aplicación como app-: app-cliente-id, app-estado, app-version. Las claves que comienzan con x-vali- están reservadas para uso interno de Vali-Blob.