Subir archivos al servidor

 En Desarrollo

En ocasiones se nos plantea la necesidad de tener que disponer de una carpeta en el servidor donde almacenar un conjunto de ficheros a tratar en algún proceso por lotes. Cuando además son distintos usuarios los que pueden ir “añadiendo” a esa carpeta diversos archivos para que sean procesados, se nos plantea una cuestión de seguridad que debemos abordar de alguna forma. Me refiero a que es posible que se conceda permisos dentro de Dynamics AX a determinados perfiles de usuario para poder “encolar” archivos para ser procesados por lotes pero:

Si se trata de un proceso por lotes, el servicio del AOS deberá tener permisos de acceso a esta carpeta. Si los archivos pueden ser añadidos por los usuarios de determinado perfil dentro de AX, ¿debemos conceder permisos a estos usuarios a dicha carpeta? Si es así, a parte de suponer un posible riesgo de seguridad, nos acarrea la tarea de que tendremos que mantener “alineados” los permisos del filesystem con los permisos de AX (mediante grupos de Active Directory por ejemplo).

¿Hay alguna otra solución que podamos llevar a cabo desde el propio Dynamics AX?

Partamos de la base de que en Dynamics AX podemos definir código que se ejecuta en Cliente y código que se ejecuta en Servidor (esto no es aplicable al 100% en la nueva versión Dynamics 365 for operations). De modo que podríamos desarrollar un pequeño fragmento de código que, ejecutándose en Cliente, leyera un archivo que indicara el usuario (incluso de su mismo PC) y luego lo traspasara a un fragmento de código que, ejecutándose en Servidor, guardara este mismo archivo en una carpeta del propio Servidor. ¿Cómo hacemos esto? Vamos a crear una clase en la que vamos a desarrollar las distintas funciones que nos van a permitir subir ficheros al servidor desde una máquina de cliente. La clase MyFileUploader.

En esta clase definiremos tres métodos estáticos:

  • UploadFile()
  • GetCheckSum()
  • SaveStreamToServer()

La idea es que, en cliente, se llama a la función UploadFile() que se encarga de leer el fichero, comprobar el checksum mediante la función GetCheckSun() y lo envía al servidor para ser almacenado mediante la función SaveStreamToServer() (que se ejecuta en Server).

La función SaveStreamToServer, una vez ha almacenado el fichero en la carpeta correspondiente, calcula el checksum de dicho fichero y lo compara con el checksum que se había calculado en cliente. Esto es tan solo una comprobación por si hubiera habido algún problema durante el upload que hubiera corrompido el fichero.

// Recibe:
// SourceFileName: la ruta del fichero a leer
// destFolder: la ruta de la carpeta en Server donde almacenar el fichero
// newFileName: Si debe renombrarse el fichero una vez en la carpeta del servidor este es el nombre
static client void UploadFile(str SourceFileName, str destFolder, str newFileName = "")
{
    #define.MaxFileSizeLimit(50000000) // 50Mb - límite de memoria
    #define.ClrFileStream ('System.IO.FileStream')
    #define.ClrFileModeEnum ('System.IO.FileMode')
    #define.ClrFileShareEnum ('System.IO.FileShare')
    #define.ClrFileModeOpen ('Open')
    #define.ClrFileAccessEnum ('System.IO.FileAccess')
    #define.ClrBinaryReader('System.IO.BinaryReader')
    #define.ClrFileAccessWrite ('Write')
    #define.ClrFileAccessRead ('Read')
    #define.ClrFileShareRead ('Read')
    #define.PacketSize(200000) //Definimos un buffer de lectura a nuestro gusto y medida

    System.Exception clrException;
    ClrObject fileStream, fileMode, fileAccess, fileShare, binaryReader, fileNameBytes, memoryStream, binaryWriter;
    System.Type typeOfByte;
    System.Array arrayOfByte;
    System.Byte arrayOfByte2;
    int arrayOfByteLength = 1;

    int64 bytesRead;
    System.Byte[] byteArray;
    str myStringByte;
    container con;
    int x=0, bytesCount=0;
    int contentlen = 0;
    clrobject   ex;
    str checksum;
    str fpath, fname, ftype;
    Set PermSet = new Set(Types::Class);
        ;

    try
    {
            PermSet.add(new FileIOPermission(SourceFileName,"RW"));
            PermSet.add(new InteropPermission(InteropKind::ClrInterop));

            CodeAccessPermission::assertMultiple(PermSet);

            if (!WinApi::fileExists(SourcefileName))
            {
                throw error(strFmt("@SYS109820",SourceFileName));
            }

            [fpath, fname, ftype] = fileNameSplit(SourceFileName);
            if (! newFileName) newFileName = fname+ftype;

            typeOfByte = System.Type::GetType("System.Byte",true);

            fileMode = CLRInterop::parseClrEnum(#ClrFileModeEnum, #ClrFileModeOpen);
            fileAccess = CLRInterop::parseClrEnum(#ClrFileAccessEnum, #ClrFileAccessRead);
            fileShare  = CLRInterop::parseClrEnum(#ClrFileShareEnum, #ClrFileShareRead);

            checksum = MyFileUploader::GetCheckSum(Sourcefilename);

            fileStream = new CLRObject(#ClrFileStream, SourcefileName, fileMode, fileAccess, fileShare);
            arrayOfByteLength = fileStream.get_Length();

            if (arrayOfByteLength > #MaxFileSizeLimit)
               throw error(strFmt("@SYS97286"));

            byteArray = new System.Byte[arrayOfByteLength]();
            binaryReader = new ClrObject(#ClrBinaryReader,fileStream);
            bytesRead = 1;

            while (bytesRead > 0)
            {
                bytesRead = binaryReader.Read(byteArray, 0,byteArray.get_Length());
                x++;
            }

            fileStream.Close(); // importante cerrar el stream para evitar bloqueos

// Ahora lo ponemos todo en un container en base64
            x=0;

            while(bytesCount<arrayOfByteLength)
            {

                if(arrayOfByteLength - bytesCount < #PacketSize)
                {
                    myStringByte=System.Convert::ToBase64String(byteArray, bytesCount, arrayOfByteLength - bytesCount);
                }
                else
                {
                    myStringByte=System.Convert::ToBase64String(byteArray, bytesCount, #PacketSize);
                }

                con+= myStringByte;
                contentlen += strlen(myStringByte);
                bytesCount += #PacketSize;
                x++;

             }

            CodeAccessPermission::revertAssert();

// Subimos el fichero

            MyFileUploader::SaveStreamToServer(newfilename, con, destFolder, x, arrayOfByteLength, checksum);

    }
    catch
    {
        ex = ClrInterop::getLastException();
        if (ex != null)
        {
            ex = ex.get_InnerException();
            if (ex != null)
            {
                throw error(ex.ToString());
            }
        }

        throw error(AifUtil::getClrErrorMessage());
    }

}

Ahora la función que nos sirve para calcular el Checksum:

// Recibe:
// file : La ruta al fichero
// Devuelve:
// un string con el checksum calculado de este fichero
public static str GetCheckSum(str file)
{
    System.IO.FileStream stream;
    System.Security.Cryptography.SHA256Managed sha;
    System.Byte[] checksum;
    System.String sret;
    str     ret = "";
    set                     PermSet = new Set(Types::Class);
    clrObject           ex;
    ;

    PermSet.add(new FileIOPermission(file,"RW"));
    PermSet.add(new InteropPermission(InteropKind::ClrInterop));

    CodeAccessPermission::assertMultiple(PermSet);

    try
    {
        stream = System.IO.File::OpenRead(file);
        sha = new System.Security.Cryptography.SHA256Managed();

        checksum = sha.ComputeHash(stream);
        sret = System.BitConverter::ToString(checksum).Replace("-", "");

        stream.Close();

        CodeAccessPermission::revertAssert();

        ret = sret;
    }
    catch(Exception::CLRError)
    {
        ex = ClrInterop::getLastException();
        if (ex != null)
        {
            ex = ex.get_InnerException();
            if (ex != null)
            {
                throw error(ex.ToString());
            }
        }

        throw error(AifUtil::getClrErrorMessage());
    }

    return ret;
}

Y por último, la función SERVER que recibe el contenido del fichero y lo guarda en una carpeta del servidor:

//Recibe:
// filename: El nombre que debe tener el fichero
// _con : El contenido del fichero en base64
// _destFolder : La ruta de la carpeta donde se almacenará el fichero
// _totalBytes: El tamaño total en bytes del fichero
// _checkSum: El checksum calculado en cliente
static private server void SaveStreamToServer(str filename, container _con ,str _destFolder, int _arrayLength, int64 _totalBytes, str _checksum)
{
    clrObject           ex, clrException;
    System.Byte[]       byteArray;
    System.Byte[]       byteAux;

    int                 x=1;
    str                 stringByte, partialStringByte;
    str sdestPath;
    FileIOPermission        fioPermission;

    System.IO.FileStream    fs;
    set                     PermSet = new Set(Types::Class);

    System.Int32            arrayIdx;
    System.Int32            BufLen;

    int iArrayIdx, iBufLen;
    int lpsb;
    str serverChecksum;
    ;

    try
    {
        sdestPath = _destFolder;
        if (substr(sdestPath,strLen(sdestPath),1) != "\\")
            sdestPath += "\\";
        sdestPath += filename;

        PermSet.add(new FileIOPermission(sdestPath,"RW"));
        PermSet.add(new InteropPermission(InteropKind::ClrInterop));
        CodeAccessPermission::assertMultiple(PermSet);

        byteArray = new System.Byte[_totalBytes]();
        ArrayIdx = 0;
        iArrayIdx = 0;

        while(x<=_arrayLength)
        {
            partialStringByte = conPeek(_con, x);
            lpsb = strlen(partialStringByte);

            byteAux = System.Convert::FromBase64String(partialStringByte);
            BufLen = byteAux.get_Length();
            iBufLen = BufLen;
            System.Buffer::BlockCopy(byteAux,0, byteArray, ArrayIdx, BufLen);

            iArrayIdx += iBufLen;
            ArrayIdx = iArrayIdx;

            x++;
        }

        fs = new System.IO.FileStream(sdestPath, System.IO.FileMode::Create);
        fs.Write(byteArray, 0, arrayIdx);
        fs.Close();

        CodeAccessPermission::revertAssert();

        serverCheckSum = MyFileUploader::GetCheckSum(sdestPath);

        if (serverCheckSum != _checksum)
            throw error("CheckSum incorrecto!!");
    }
    catch(Exception::CLRError)
    {
        ex = ClrInterop::getLastException();

        if (ex != null)
        {
            ex = ex.get_InnerException();
            if (ex != null)
            {
                throw error(ex.ToString());
            }
        }

        throw error(AifUtil::getClrErrorMessage());
    }
    catch (Exception::Internal)
    {
        clrException = CLRInterop::getLastException();
        throw error(clrException.get_Message());
    }
}

Límite de tamaño

Si os habéis fijado, hemos establecido un límite de tamaño para el fichero a subir. La razón de este límite es porque, como se puede ver en el código, lo que estamos haciendo es leer el fichero en memoria y traspasarlo al servidor donde se volverá a volcar a disco de nuevo. Este traspaso del contenido del fichero en memoria, si no se maneja bien, puede producir problemas tanto en el cliente como incluso en el servidor, provocando caídas del servicio AOS, etc. De hecho, existen unos parámetros que hay que modificar en el registro de windows del servidor si queremos establecer el límite en 50Mb como hemos hecho nosotros (por defecto el límite del sistema son 10Mb).

Para poder alterar este límite, en la máquina del AOS, hay que:

Localizar la clave de registro de windows correspondiente a:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Dynamics Server\5.0

Dentro, elegir la carpeta correspondiente a la instancia y configuración en la que estamos realizando el desarrollo. Una vez en la carpeta correspondiente, hay que modificar la clave MaxBufferSize de tipo String (si no existe hay que crearla). Establecer un valor (en bytes) que sea algo mayor al límite de tamaño de fichero que deseamos poder subir. Con esto ya debería ser suficiente para conseguir subir ficheros desde cualquier máquina cliente al servidor sin necesidad de otorgar permisos a cada uno de los usuarios a la carpeta del servidor (tan solo al usuario del servicio de AOS).

Para probarlo nos basta con crear un job como el que sigue:

static void QN_TestUploader(Args _args)
{
    str filename = "C:\\Users\\mquerol\\Desktop\\Notas.txt"; // La ruta al fichero que deseeis subir
    str carpetaServer = "C:\\Uploads"; // La carpeta en el servidor donde alojar el archivo
    ;

   MyFileUploader::UploadDocument(filename, carpetaServer, "NotasServidor.txt");

}

Al ejecutarlo debería aparecer el fichero en la carpeta del servidor.

Saludos.

Publicaciones Recientes

Dejar comentario