Saltar al contenido principal

Listado de Archivos

Vali-Blob proporciona dos operaciones para explorar el contenido del almacenamiento: ListFilesAsync para obtener archivos y ListFoldersAsync para obtener prefijos de "carpeta". En almacenamiento en la nube no existen carpetas reales — son convenciones de nomenclatura basadas en prefijos comunes.

Métodos disponibles

// Listar todos los archivos bajo un prefijo dado
Task<StorageResult<IReadOnlyList<FileEntry>>> ListFilesAsync(
string prefix,
CancellationToken ct = default);

// Listar prefijos de "carpeta" directamente bajo un prefijo dado
Task<StorageResult<IReadOnlyList<string>>> ListFoldersAsync(
string prefix,
CancellationToken ct = default);

FileEntry — estructura

public class FileEntry
{
/// <summary>Ruta completa del archivo.</summary>
public required string Path { get; init; }

/// <summary>Nombre del archivo (sin la ruta de directorio).</summary>
public string FileName => StoragePath.GetFileName(Path);

/// <summary>Tamaño del archivo en bytes.</summary>
public long SizeBytes { get; init; }

/// <summary>Tipo MIME del archivo, si el proveedor lo devuelve.</summary>
public string? ContentType { get; init; }

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

/// <summary>ETag del archivo.</summary>
public string? ETag { get; init; }
}

Campos de FileEntry

CampoTipoDescripción
PathstringRuta completa del archivo en el almacenamiento
FileNamestringSolo el nombre del archivo (calculado desde Path)
SizeByteslongTamaño del archivo en bytes
ContentTypestring?Tipo MIME (disponibilidad varía por proveedor)
LastModifiedDateTimeOffset?Fecha de última modificación
ETagstring?Hash de integridad del proveedor

ListFilesAsync

Listar todos los archivos en la raíz

// Pasar cadena vacía para listar desde la raíz
var resultado = await storage.ListFilesAsync("", ct);

if (resultado.IsSuccess)
{
Console.WriteLine($"Total de archivos: {resultado.Value!.Count}");
foreach (var archivo in resultado.Value!)
{
Console.WriteLine($" {archivo.Path} ({archivo.SizeBytes:N0} bytes)");
}
}

Listar archivos en una carpeta específica

// El prefijo debe terminar en "/" para listar el contenido de una carpeta
var resultado = await storage.ListFilesAsync("documentos/2024/", ct);

if (resultado.IsSuccess)
{
var archivos = resultado.Value!;
var totalBytes = archivos.Sum(a => a.SizeBytes);

Console.WriteLine($"Documentos en 2024: {archivos.Count}");
Console.WriteLine($"Espacio usado: {totalBytes / 1024.0 / 1024.0:F2} MB");

foreach (var archivo in archivos.OrderByDescending(a => a.LastModified))
{
Console.WriteLine($" {archivo.FileName,-40} {archivo.LastModified:d} {archivo.SizeBytes / 1024:N0} KB");
}
}

Listar archivos de un tenant específico

public async Task<IReadOnlyList<FileEntry>> ListarArchivosClienteAsync(
IStorageProvider storage,
string clienteId,
string? categoria,
CancellationToken ct)
{
var prefijo = categoria is not null
? $"tenants/{clienteId}/{categoria}/"
: $"tenants/{clienteId}/";

var resultado = await storage.ListFilesAsync(prefijo, ct);
return resultado.IsSuccess ? resultado.Value! : [];
}

Filtrar por tipo de contenido

var resultado = await storage.ListFilesAsync("subidas/", ct);

if (resultado.IsSuccess)
{
var soloImagenes = resultado.Value!
.Where(f =>
f.ContentType?.StartsWith("image/") == true ||
new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif" }
.Contains(Path.GetExtension(f.Path).ToLowerInvariant()))
.OrderBy(f => f.LastModified)
.ToList();

Console.WriteLine($"Imágenes encontradas: {soloImagenes.Count}");
}

Calcular uso de almacenamiento

public async Task<(long TotalArchivos, long TotalBytes)> CalcularUsoAsync(
IStorageProvider storage,
string prefijo,
CancellationToken ct)
{
var resultado = await storage.ListFilesAsync(prefijo, ct);
if (!resultado.IsSuccess) return (0, 0);

var archivos = resultado.Value!;
return (archivos.Count, archivos.Sum(a => a.SizeBytes));
}

// Uso
var (total, bytes) = await CalcularUsoAsync(storage, $"tenants/{tenantId}/", ct);
Console.WriteLine($"Uso del tenant: {total} archivos, {bytes / 1024.0 / 1024.0:F2} MB");

ListFoldersAsync

ListFoldersAsync retorna los prefijos de "carpeta" directamente bajo el prefijo dado. Cada resultado es una cadena que representa el prefijo de la subcarpeta.

// Listar carpetas en la raíz
var resultado = await storage.ListFoldersAsync("", ct);

if (resultado.IsSuccess)
{
foreach (var carpeta in resultado.Value!)
Console.WriteLine($"Carpeta: {carpeta}");
// Ejemplos: "documentos/", "imagenes/", "backups/", "tenants/"
}

// Listar subcarpetas de "tenants/"
var subCarpetas = await storage.ListFoldersAsync("tenants/", ct);
// Ejemplos: ["tenants/acme/", "tenants/globex/", "tenants/initech/"]

Paginación

Vali-Blob retorna todos los resultados en una sola llamada, manejando automáticamente la paginación interna de cada proveedor. Para buckets grandes, implementa paginación en la capa de aplicación:

Paginación por offset

public async Task<(IEnumerable<FileEntry> Archivos, int TotalCount)> ListarPaginadoAsync(
IStorageProvider storage,
string prefijo,
int pagina,
int tamanoPagina,
CancellationToken ct)
{
var resultado = await storage.ListFilesAsync(prefijo, ct);
if (!resultado.IsSuccess) return ([], 0);

var todos = resultado.Value!.OrderBy(f => f.Path).ToList();
var paginados = todos
.Skip(pagina * tamanoPagina)
.Take(tamanoPagina);

return (paginados, todos.Count);
}

Paginación eficiente usando prefijos de fecha

// Organización recomendada: subidas/AAAA/MM/DD/archivo.ext
// Permite paginar por mes o día sin cargar todos los archivos

public async Task<IReadOnlyList<FileEntry>> ListarPorMesAsync(
IStorageProvider storage,
int ano,
int mes,
CancellationToken ct)
{
var prefijo = $"subidas/{ano:D4}/{mes:D2}/";
var resultado = await storage.ListFilesAsync(prefijo, ct);
return resultado.IsSuccess ? resultado.Value! : [];
}

public async Task<IReadOnlyList<FileEntry>> ListarPorDiaAsync(
IStorageProvider storage,
DateOnly fecha,
CancellationToken ct)
{
var prefijo = $"subidas/{fecha:yyyy/MM/dd}/";
var resultado = await storage.ListFilesAsync(prefijo, ct);
return resultado.IsSuccess ? resultado.Value! : [];
}

Recorrido recursivo del árbol de carpetas

public async Task RecorrerArbolAsync(
IStorageProvider storage,
string prefijo,
Func<FileEntry, Task> procesarArchivo,
CancellationToken ct,
int profundidadMaxima = 10,
int profundidadActual = 0)
{
if (profundidadActual >= profundidadMaxima) return;

// Procesar archivos en el nivel actual
var archivos = await storage.ListFilesAsync(prefijo, ct);
if (archivos.IsSuccess)
{
foreach (var archivo in archivos.Value!)
await procesarArchivo(archivo);
}

// Descender en subcarpetas
var carpetas = await storage.ListFoldersAsync(prefijo, ct);
if (carpetas.IsSuccess)
{
foreach (var carpeta in carpetas.Value!)
{
await RecorrerArbolAsync(storage, carpeta, procesarArchivo, ct,
profundidadMaxima, profundidadActual + 1);
}
}
}

Explorador de archivos como API REST

app.MapGet("/api/explorador", async (
string? prefijo,
IStorageProvider storage,
CancellationToken ct) =>
{
var prefijoActual = prefijo ?? "";

var carpetasTask = storage.ListFoldersAsync(prefijoActual, ct);
var archivosTask = storage.ListFilesAsync(prefijoActual, ct);
await Task.WhenAll(carpetasTask, archivosTask);

var carpetas = carpetasTask.Result.IsSuccess ? carpetasTask.Result.Value! : [];
var archivos = archivosTask.Result.IsSuccess ? archivosTask.Result.Value! : [];

return Results.Ok(new
{
prefijo = prefijoActual,
carpetas = carpetas.Select(c => new
{
nombre = c.TrimEnd('/').Split('/').Last(),
ruta = c,
tipo = "carpeta"
}),
archivos = archivos.Select(f => new
{
nombre = f.FileName,
ruta = f.Path,
tamanoBytes = f.SizeBytes,
tamanoMb = Math.Round(f.SizeBytes / 1024.0 / 1024.0, 3),
tipoContenido = f.ContentType,
ultimaModificacion = f.LastModified
}),
totalArchivos = archivos.Count,
totalBytes = archivos.Sum(f => f.SizeBytes)
});
});

Consideraciones de rendimiento por proveedor

ProveedorLímite interno por solicitudPaginación automática en Vali-Blob
Amazon S31,000 objetos
Azure Blob Storage5,000 blobs
Google Cloud Storage1,000 objetos
OCI Object Storage1,000 objetos
Supabase StorageVariable
Sistema de archivos localSin límiteN/A
Información

Vali-Blob gestiona automáticamente la paginación interna de cada proveedor. ListFilesAsync devuelve todos los resultados independientemente de los límites internos del proveedor, realizando múltiples solicitudes si es necesario. Para buckets con millones de archivos, usa prefijos específicos para limitar el alcance de las consultas.

Consejo

Adopta desde el inicio una convención de nomenclatura de rutas consistente. El patrón entidad/{id}/categoria/archivo.ext o el uso de prefijos de fecha AAAA/MM/DD/archivo.ext te permitirá filtrar y listar eficientemente sin necesidad de cargar todos los metadatos del bucket.