1Diseñar la jerarquía de directorios

Path representa una ruta sin tocar el disco; Files es quien hace las operaciones reales de E/S. Compón siempre con resolve(), nunca concatenando cadenas con +.

java
Path raiz = Path.of(directorioRaiz); // externalizada, ver paso 5
Path directorioExpediente = raiz
        .resolve(String.valueOf(anio))
        .resolve(String.format("%02d", mes))
        .resolve(provincia);

Files.createDirectories(directorioExpediente); // crea intermedios si faltan
Path vs. FilesPath.of(...) / Paths.get(...) no requieren que el fichero exista. Todo lo que sí toca el disco (crear, comprobar, mover, borrar) vive en Files.

2archivar(Expediente)

Crea los directorios intermedios que falten antes de escribir el expediente.

java
public void archivar(Expediente expediente) {
    Path directorio = calcularDirectorio(expediente);
    Files.createDirectories(directorio); // no falla si ya existen
    Path fichero = directorio.resolve(expediente.getNumero() + ".json");
    // serializar y escribir expediente en fichero
}

3buscar(numeroExpediente)

Sin recorrido manual del árbol: apóyate en Files.walk o Files.find con filtro, dentro de un try-with-resources (el stream mantiene descriptores de directorio abiertos).

java
public Optional<Path> buscar(String numeroExpediente) throws IOException {
    String nombre = numeroExpediente + ".json";
    try (Stream<Path> stream = Files.walk(raiz)) {
        return stream
                .filter(Files::isRegularFile)
                .filter(p -> p.getFileName().toString().equals(nombre))
                .findFirst();
    }
}

4mover(numeroExpediente, EstadoExpediente)

Cambiar el estado de un expediente implica moverlo de directorio con Files.move y las opciones apropiadas.

java
public void mover(String numeroExpediente, EstadoExpediente nuevoEstado) throws IOException {
    Path origen = buscar(numeroExpediente)
            .orElseThrow(() -> new NoSuchFileException(numeroExpediente));
    Path destino = calcularDirectorioPorEstado(nuevoEstado).resolve(origen.getFileName());
    Files.createDirectories(destino.getParent());
    Files.move(origen, destino, StandardCopyOption.REPLACE_EXISTING);
}

5Raíz configurable

Externaliza el directorio raíz con @ConfigurationProperties en vez de escribir la ruta en el código: cambia por entorno sin recompilar.

java
@ConfigurationProperties(prefix = "app.almacenamiento")
public class AlmacenamientoProperties {
    private String directorioExpedientes;
    // getters y setters
}
yaml
app:
  almacenamiento:
    directorio-expedientes: /var/datos/expedientes

6Endpoints REST

El controlador delega en el servicio: no debe tocar Files/Path directamente.

java
@RestController
@RequestMapping("/expedientes")
@RequiredArgsConstructor
public class ExpedienteController {

    private final AlmacenExpedientes almacen;

    @GetMapping("/{numero}/archivo")
    public ResponseEntity<Resource> archivo(@PathVariable String numero) {
        return almacen.buscar(numero)
                .map(p -> ResponseEntity.ok((Resource) new FileSystemResource(p)))
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping("/{numero}/estado")
    public ResponseEntity<Void> cambiarEstado(@PathVariable String numero,
                                                @RequestBody EstadoExpediente nuevoEstado) {
        almacen.mover(numero, nuevoEstado);
        return ResponseEntity.ok().build();
    }
}
Sin acceso a ficheros en el controladorNi Path ni Files deben aparecer en la clase @RestController: toda la E/S vive en el servicio.

7Pruebas con directorio temporal

Usa Files.createTempDirectory para que cada ejecución de pruebas empiece limpia y no interfiera con otras.

java
@TempDir
Path directorioTemporal;

@Test
void archivaEnLaRutaEsperada() throws IOException {
    AlmacenExpedientes almacen = new AlmacenExpedientes(directorioTemporal.toString());
    almacen.archivar(expedienteDePrueba());
    assertThat(almacen.buscar(expedienteDePrueba().getNumero())).isPresent();
}

!Problemas frecuentes

Rutas con separador a manoConcatenar raiz + "/" + anio + "/" + mes rompe en Windows. Usa siempre resolve().
Files.move sin crear el destinoFiles.move no crea directorios intermedios: llama a Files.createDirectories(destino.getParent()) antes.

✓Criterios de aceptación

  • Las rutas se calculan con la API Path/Files, nunca concatenando cadenas con +
  • archivar crea los directorios intermedios que falten
  • buscar no hace recorrido manual del árbol de directorios
  • El directorio raíz es configurable por propiedad, no está hardcodeado
  • Las pruebas usan un directorio temporal que se limpia solo
  • Los endpoints devuelven los códigos HTTP correctos (200/404)
  • El controlador delega toda la E/S en el servicio

Adaptado de apartado b — Path, Paths y Files, apartado f — Excepciones y configuración y apartado h — Actividades.