Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

Creando ficheros temporales

¿Ha necesitado alguna vez un fichero temporal? Me acaba de suceder: estaba haciendo chapuzas con el componente TNMHTTP, de la página FastNet, para ejecutar una aplicación CGI desde Internet. Necesitaba pasarle parámetros a la aplicación utilizando el método POST, y encontré un procedimiento dentro del componente mencionado que lo hace. Casualmente, el método del componente también se llama Post. El primer parámetro es la URL de la aplicación que se va a ejecutar (a través del servidor HTTP, por supuesto), y el segundo parámetro aparece escuetamente documentado como PostData, del tipo string.

MIS PROBLEMAS CON UN COMPONENTE

Bueno, hay un poco más de información: si la propiedad OutputFileMode del componente vale False, el parámetro se interpreta directamente como la lista de parámetros a pasar. En caso contrario, se asume que el parámetro corresponde al nombre de un fichero de texto, que es el que entonces contiene los parámetros. Si, por ejemplo, quieres decirle a la aplicación CGI que el identificador de conexión vale 1984, pones False en OutputFileMode, y pasas la cadena 'connid=1984' en el segundo parámetro de Post.

Pero en ningún maldito lugar te explican qué tienes que hacer para pasar más de un parámetro a la aplicación CGI. Quiero hacer constar que probé todas las posibilidades, desde las más lógicas hasta las completamente extravagantes. Es decir, desde separar los parámetros con ampersands hasta embutir cambios de línea dentro de la cadena. Y nada.

Afortunadamente, si creamos un fichero de texto utilizando una línea separada para cada parámetro, el envío de datos funciona a la perfección. Estoy hablando, por supuesto, de asignar True a la propiedad OutputFileMode del componente, y de pasar en el segundo parámetro Post el nombre del fichero de texto.

Tengo muy buena opinión sobre las habilidades de programación de los empleados de Borland. Pero me cabrea comprobar otra vez lo mal que diseñan y programan las compañías que colaboran con Borland para la creación de ciertos componentes. Este problema es, más que un bug comprensible, un mal diseño de la interfaz de la clase del componente. Quiero aclarar también que, un mes después de escribir este truco, escribí la versión tres de la aplicación que he mencionado, y sustituí el componente de FastNet por su equivalente en Indy.

FICHEROS TEMPORALES

Para poder llamar al método Post del componente cliente de HTTP, mi aplicación crea un fichero temporal de texto y escribe en él una línea por cada parámetro que quiere pasar. Lo más importante ahora es que el nombre de ese fichero temporal debe ser elegido con sumo cuidado. Aunque no quiero entrar en detalles aquí y ahora, la aplicación que estaba escribiendo es también una aplicación CGI, que puede en teoría ser ejecutada simultáneamente como respuesta a pedidos de usuarios concurrentes. Si utilizo el mismo nombre de fichero, puedo encontrar un conflicto. De lo que se trata, en definitiva, es de generar un nombre de fichero que sea único, a ciencia cierta.

Hay varias posibilidades, pero empezaré por la más "tradicional". Analice el contenido de la siguiente función:

function CreateTemporalFile(const APrefix: string): string;
var
   TempPath, TempFile: array [0..MAX_PATH-1] of Char;
begin
   GetTempPath(SizeOf(TempPath), TempPath);
   if GetTempFileName(TempPath, PChar(APrefix), 0, TempFile) = 0 then
      RaiseLastWin32Error;
   Result := string(TempFile);
end;

Tenemos, en primer lugar, la llamada a GetTempPath, que nos sirve para averiguar cuál es el directorio temporal utilizado por Windows. A continuación se llama a GetTempFileName. Como primer parámetro se pasa el directorio en el que queremos ubicar nuestro fichero. A continuación, podemos pasar una cadena de hasta 3 caracteres para asignarle un prefijo al nombre que se va a generar. ¿Me permite saltarme el tercer parámetro? El cuarto sirve para recibir el nombre del fichero generado. Finalmente, si el valor devuelto por la función es cero (ahora explico qué significa) quiere decir que la función ha fallado, y llamamos a RaiseLastWin32Error, un procedimiento de Delphi que se encarga de lanzar una excepción e interrumpir el flujo del programa. Como debe ser.

Volvamos al tercer parámetro. Si su valor es distinto de cero, se utiliza para completar el nombre del fichero. Si pasamos 1, por ejemplo, y el prefijo es im, el nombre generado sería 'im0001.tmp' (con la ruta incorporada, claro). Es decir, seríamos nosotros los responsables de garantizar que el nombre del fichero sea único. Es una mala técnica para una aplicación CGI, como es fácil de ver, porque los números tendrían que ser únicos incluso desde una ejecución del programa a la siguiente. Y tenemos que contar con la posibilidad de que se nos haya quedado algún fichero sin borrar en ese directorio.

Por lo tanto, Windows nos echa una mano cuando pasamos 0 en ese parámetro. En tal caso, es el sistema operativo el que genera un número basado en la hora del sistema, para hacer poco probable una colisión. Pero para garantizar al 100% que ésta no se produce, Windows intenta crear el fichero a cuenta nuestra. No hay mejor forma de saber si un huevo está podrido que rompiéndolo. Si encuentra problemas, incrementa el valor en uno y sigue probando hasta que triunfa en su empeño. Al finalizar, nos devuelve un nombre de fichero único ... ¡y además lo deja ya creado! Será nuestra aplicación la responsable de eliminar el fichero al terminar de usarlo. No voy a entrar en detalles, pero le sugiero que almacene el nombre del fichero en una lista de cadenas global, y que cree una segunda función para simultáneamente borrar el fichero del disco y de la lista. Para terminar, el código de finalización de alguna unidad puede encargarse de borrar los ficheros que por errores de programación hayamos dejado sin cerrar al irnos.

SEGUNDO INTENTO

Hay otra forma de obtener nombres para ficheros temporales, aunque no es obligatoriamente mejor que la que acabamos de explicar. ¿Cuál es el principal problema de GetTempFileName? Pues que para garantizar que el nombre pergeñado por el sistema es realmente único tiene que acceder físicamente al directorio. Debe, además, dejar creado el fichero para que otra llamada simultánea no devuelva el mismo valor. Y eso tiene su coste en tiempo.

Se me ocurre otra idea: ¿no nos dice Microsoft que los identificadores únicos de COM deben ser siempre únicos? Entonces nos bastará con llamar a las funciones de generación de GUIDs para obtener un nombre único ... sin necesidad de tocar el disco duro:

function GetTemporalFile: string;
var
   TempPath: array [0..MAX_PATH-1] of Char;
   G: TGuid;
begin
   GetTempPath(SizeOf(TempPath), TempPath);
   OleCheck(CoCreateGuid(G));
   Result := string(TempPath) + GuidToString(G);
end;

Luego, cuando vayamos a crear el fichero, podemos utilizar la función CreateFile, del API de Windows, y hacer uso de la opción FILE_FLAG_DELETE_ON_CLOSE. Los ficheros creados con este atributo se borran automáticamente cuando se cierran. Tenga cuidado de no confundirse y utilizar FILE_ATTRIBUTE_TEMPORARY, que tiene que ver en realidad con la caché de escritura en disco.

¿Alguna pega en contra de este método? Sí: el nombre generado para el fichero es un nombre largo. Si la aplicación se ejecutará en una partición FAT, debe recordar que en este tipo de particiones los nombres largos se implementan algo ineficientemente utilizando entradas contiguas de 32 bytes en la estructura del directorio. Puede que esta penalización sea menor al final que la correspondiente al primer método de generación, pero hay que evaluar el riesgo de provocar una mayor fragmentación del espacio del disco duro.