Integridad de ficheros (Respuesta larga)

Salvador Ortiz Garcia sog@msg.com.mx
Fri, 6 Mar 1998 21:10:12 -0600 (CST)


On Fri, 6 Mar 1998, Jose Ignacio wrote:

> Bueno a ver si ahora voy bien. Lo que quiero es leer todo el fichero,
> modificar una variable y volver a escribir todo el fichero. He recibido 
> varios mails con lo cual observo que es un problema común a varias
> personas.

Por fín sé lo que quieres hacer, y así es más fácil ayudarte.

Primero y por completez, algunos comentarios generales respecto a la
lectura y escritura en un fichero (archivo).

Cuando abres un fichero para lectura/escritura necesitas mover el
apuntador interno del archivo para cambiar de una a otra.

Del manual de fopen(2):

       Reads  and  writes may be intermixed on read/write streams
       in any order.  Note that ANSI C requires that a file posi-
       tioning  function  intervene  between  output  and  input,
       unless an input  operation  encounters  end-of-file.   (If
       this  condition  is  not  met,  then  a read is allowed to
       return the result of writes other than the  most  recent.)
       Therefore it is good practice to put an  fseek or  fgetpos
       operation  between  write  and  read  operations on such a
       stream.  This operation  may  be  an apparent no-op (as in
       fseek(..., 0L, SEEK_CUR) called for its synchronizing side
       effect.

Lo anterior traducido a perl, quiere decir que:

1) No necesitas abrir nuevamente el archivo, de hecho, si lo haces
   te metes en otros lios.
2) Si quieres reescribir sobre lo leído necesitas posicionarte al
   principio del archivo con:

   seek(FF,0,0);

Ahora, para tu caso en particular, la cosa se complica con este esquema 
por que si escribes menos de lo que leíste, al final de lo reescrito
quedará en el archivo como basura parte del contenido anterior.

Para lograr lo que quieres necesitas usar un esquema parecido al usado por
los programas que manipulan en UNIX el archivo /etc/passwd.

La idea general es la siguiente:

1. Cualquiera puede abrir y leer del archivo sin preocuparse por la
concurrencia de procesos.   

2. Cuando algún proceso necesita modificar el archivo, crea un pseudo-lock
que debe ser respetado por otros procesos que quieran hacer
modificaciones.

3. Teniendo el control del pseudo-lock, se lee todo el archivo y se
escribe la versión modificada en otro archivo temporal.

4. Al terminar, se sustituye el original por el creado y se libera el
pseudo-lock.

Al implementar el esquema descrito se tiene que tener cuidado de evitar
"carreras" entre los procesos cuidando la atomizidad de algunas
operaciones, lo que hace que la implementación sea dependiente del sistema
operativo usado.

A continuación el código de una implementación usando perl 5.004 estándar 
o superior, para cualquier sistema operativo que cumpla POSIX a la que
sólo le falta que pongas tu código en los huecos indicados:

======= Corta aquí ==========
#!/usr/bin/perl -w
use IO::File;

sub create_new {
	# Función para crear archivo temporal y  pseudo-lock en
        # forma atómica.
	# Recibe nombre del archivo original
	my $file = shift;
	my $fh;
	while(1) {
		$fh = IO::Open("$file.tmptmp", O_WRONLY|O_CREAT|O_EXCL,0644);
		last if(defined($fh) || link("$file.tmptmp","$file.tmp"));
		# Error en creación de pseudo-lock, alguien está modificando 
		# $file, me espero un segundo y reintento.
		if(defined($fh)) {
			$fh->close;
			unlink("$file.tmptmp");
		}
		sleep(1);
	}
	unlink("$file.tmptmp");   # Borro el temporal, sin cerrarlo!
	return $fh;               # Regreso el FILEHANDLE del temporal
}

sub actualiza {  
	my $file = shift;
	my $fh	 = shift;
	$fh->close;	           # Cierra temporal
	rename("$file.tmp",$file); # Cambia atómicamente el viejo
	                           # liberando al vuelo el pseudo-lock
}

# El código siguiente es para actualizar el archivo.
$archivo = '/el/nombre/de/tu/archivo'; # Nombre completo de tu archivo
$NUEVO = create_new($archivo);	       # Creas temporal y pseudo-lock
open(FF,"<$archivo");		       # Abres tu archivo normalmente
while(<FF>) {			       # Lees tu archivo normalmente
	...
}
for(noseque) {			       # Escribes en $NUEVO lo que quieras
	print $NUEVO "lo que quieras";
	...
}

close(FF);				# Cierro FF normalmente
actualiza($archivo,$NUEVO);		# Actualiza y cierra $NUEVO
# Listo.
======= Fin de código =======

Si las caracteristicas de tu plataforma no corresponden a las mencionadas
(por ejemplo en MSWindows) puede requerir algunos ajustes (o que cambies
de plataforma ;-) , pero en principio, ahí está lo que buscas.

El por qué el método funciona, queda como ejercicio para el lector :-) 
 

Saludos y suerte

Salvador Ortiz.