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.
|