Se requiere implementar una funcionalidad que genere localmente una estructura de directorios anidada —basada en una representación arbórea previamente construida—, descargue archivos adjuntos asociados a cada nodo y finalmente comprima todo el contenido en un archivo ZIP listo para descarga.
La solución se basa en tres etapas clave: (1) obtención recursiva de la estructura jerárquica desde una API o servicio de árbol; (2) creación física de carpetas y descarga de recursos vinculados a cada nodo hoja; y (3) compresión del directorio resultante usando java.util.zip, sin dependencias externas.
Controlador REST para activación del flujo
@ApiOperation("Exporta resultados organizados como árbol en paquete ZIP")
@PostMapping("/export/structured-zip")
public ResponseEntity<ResponseEntity.BodyBuilder> generateAndDownloadZip(
@RequestParam String projectId,
@RequestParam String departId) {
List<TreeNode> treeData = treeService.fetchFullTree(projectId, departId);
String zipPath = zipGenerator.generateZipFromTree(treeData, "resultados-proyecto");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + zipPath.split("/")[zipPath.split("/").length - 1] + "\"")
.body(EntityResponse.of(zipPath));
}
Construcción recursiva de la jerarquía de carpetas y descarga de adjuntos
El método buildDirectoryStructure recorre cada nodo, crea su carpeta correspondiente y, si es un nodo terminal (leaf), busca y descarga los archivos relacionados mediante llamadas HTTP sincrónicas:
private void buildDirectoryStructure(List<TreeNode> nodes, Path parentDir) throws IOException {
for (TreeNode node : nodes) {
Path nodeDir = Files.createDirectories(parentDir.resolve(node.getLabel()));
if (node.isLeaf()) {
downloadAttachmentsForNode(node.getId(), nodeDir);
} else {
buildDirectoryStructure(node.getChildren(), nodeDir);
}
}
}
private void downloadAttachmentsForNode(String nodeId, Path targetDir) throws IOException {
List<AttachmentMetadata> attachments = attachmentService.findByNodeId(nodeId);
RestTemplate client = new RestTemplate();
attachments.forEach(attachment -> {
try {
byte[] content = client.getForObject(attachment.getDownloadUrl(), byte[].class);
String safeName = sanitizeFilename(attachment.getOriginalName());
Files.write(targetDir.resolve(safeName), content);
} catch (Exception e) {
log.warn("Fallo al descargar adjunto {}: {}", attachment.getId(), e.getMessage());
}
});
}
Compresión ZIP con soporte para estructura de directorios
La clase ZipArchiver utiliza ZipOutputStream para empaqueatr de forma recursiva, preservando la ruta relativa de cada entrada:
public Path createZipArchive(Path sourceDir, String archiveName) throws IOException {
Path zipFile = Files.createTempFile(archiveName, ".zip");
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipFile))) {
compressRecursively(sourceDir, zos, "");
}
return zipFile;
}
private void compressRecursively(Path file, ZipOutputStream zos, String base) throws IOException {
String entryName = base.isEmpty() ? file.getFileName().toString() : base + "/" + file.getFileName();
if (Files.isRegularFile(file)) {
zos.putNextEntry(new ZipEntry(entryName));
Files.copy(file, zos);
zos.closeEntry();
} else if (Files.isDirectory(file)) {
zos.putNextEntry(new ZipEntry(entryName + "/"));
zos.closeEntry();
try (Stream<Path> children = Files.list(file)) {
children.forEach(child -> {
try {
compressRecursively(child, zos, entryName);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
}
Limpieza post-compresión opcional
Para evitar acumulación de archivos temporales, se incluye un limpiador seguro que elimina solo el directorio generado (no recursivo sobre rutas arbitrarias):
private void cleanupTemporaryDirectory(Path dir) {
try {
if (Files.exists(dir) && Files.isDirectory(dir)) {
Files.walk(dir)
.sorted(Comparator.reverseOrder())
.forEach(path -> {
try { Files.delete(path); }
catch (IOException ignored) {}
});
}
} catch (IOException ignored) {}
}
Este enfoque garantiza reproducibilidad, control explícito sobre permisos de escritura y compatibilidad nativa con sistemas operativos heterogéneos, evitando librerías de terceros como Apache Commons Compress o TrueZIP.