Sencillo cliente FTP

Iniciado por V1c70r, Julio 02, 2020, 02:45:50 AM

Tema anterior - Siguiente tema

0 Miembros y 2 Visitantes están viendo este tema.

Hola a todos/as!

Esta es mi primera entrada aquí y quería compartir con vosotros un sencillo cliente para un servidor FTP. Estoy aprendiendo a programar en Python y pensé que una buena manera de aprender es compartir mi código y mirar las opiniones de los demás para ir acumulando ideas y consejos.

Un saludo!

Código: python

from ftplib import FTP
import os
from getpass import getpass

class ftpClient():
    def __init__(self):
        while True: 
            try:
                self.host = str(input("Host address: "))
                self.ftp = FTP(self.host)
                print(self.ftp.getwelcome())
                break
            except:
                print("Couldn't connect to host")
                pass

        self.user = input("Username: ")
        self.passwd = getpass()
        self.ftp.login(self.user, self.passwd)
       
       
   
    def menu(self):
        os.system("cls")
        print("""
           
             ___    _____  ___       ___    _                     _   
            (  _`\ (_   _)(  _`\    (  _`\ (_ )  _               ( )_
            | (_(_)  | |  | |_) )   | ( (_) | | (_)   __    ___  | ,_)
            |  _)    | |  | ,__/'   | |  _  | | | | /'__`\/' _ `\| | 
            | |      | |  | |       | (_( ) | | | |(  ___/| ( ) || |_
            (_)      (_)  (_)       (____/'(___)(_)`\____)(_) (_)`\__)

            1. Download Files for FTP server
            2. Upload Files for FTP server
        """)

#LLama al metodo menu() para despues pregunta la opción que deseamos, valida el valor de entrada y llama al metodo correspondiente.
    def main(self):
        self.menu()
        while True:
            self.option = int(input("What is your option? > "))
            if self.option == 1:
                self.downloadFile()
                break
            elif self.option == 2:
                self.uploadFile()
                break
            else:
                print("Incorrect options, try again!")

# Imprime archivos y directorios remotos, descarga el contenido del fichero citado y lo copia a uno nuevo.
# Por ultimo imprime el contenido del directorio local
    def downloadFile(self):
        self.ftp.retrlines("LIST")
        self.file = str(input("Which file? > "))
        try:
            self.ftp.retrbinary("RETR " + self.file, open(self.file, "wb").write)
            print(os.listdir())
            print("Succesful download")
        except:
            print("An error ocurred :(")
   
#Imprime los archivos locales del directorio en el que nos encontramos, abrimos el archivo en modo lectura binario para luego pasarlo ....
#Como parametro de ftp.storbinary para subirlo después.
    def uploadFile(self):
        print(os.listdir())
        self.file = str(input("Which file > "))
        self.open = open(self.file, "rb")
        try:
            self.ftp.storbinary("STOR " + self.file, self.open)
            self.ftp.retrlines("LIST")
            print("Succesful upload!")
        except:
            print("An error ocurred")

if __name__ == "__main__":     
    client = ftpClient()
    client.main()



Igual como siguiente paso, podrias programar el cliente ftp sin la libreria ftp. Utilizando sockets y viendo como es el estandar para hacer una implementacion tuya.
Igual no es cuestion de hacer todo el standard pero podrias implementar parte de las acciones que hace el cliente. Podrias incluir algo asi como una parte grafica. Las aplicaciones graficas siempre motivan un monton mas que las aplicaciones de linea de comando, al menos al principio.
Es solo una idea, la cuestion es seguir programando y aprendiendo.

Lo siento, no contesto dudas por MP, si tienes dudas las planteas en el foro.

No tienes permitido ver los links. Registrarse o Entrar a mi cuenta
Igual como siguiente paso, podrias programar el cliente ftp sin la libreria ftp. Utilizando sockets y viendo como es el estandar para hacer una implementacion tuya.
Igual no es cuestion de hacer todo el standard pero podrias implementar parte de las acciones que hace el cliente. Podrias incluir algo asi como una parte grafica. Las aplicaciones graficas siempre motivan un monton mas que las aplicaciones de linea de comando, al menos al principio.
Es solo una idea, la cuestion es seguir programando y aprendiendo.
Gracias por tu opinión animanegra, la próxima vez intentaré hacerlo desde el módulo socket para ver cómo me queda.

Enviado desde mi Redmi 8 mediante Tapatalk


Julio 02, 2020, 06:45:49 AM #3 Ultima modificación: Julio 02, 2020, 06:50:43 AM por DtxdF
Hola @No tienes permitido ver los links. Registrarse o Entrar a mi cuenta  ;D, muchas gracias por publicar tu código y será un gusto analizarlo.

Comencemos con el método inicial "__init__":

Código: python
def __init__(self):
        while True: 
            try:
                self.host = str(input("Host address: "))
                self.ftp = FTP(self.host)
                print(self.ftp.getwelcome())
                break
            except:
                print("Couldn't connect to host")
                pass

        self.user = input("Username: ")
        self.passwd = getpass()
        self.ftp.login(self.user, self.passwd)


Mayormente no se debe interactuar con el usuario en el constructor de clase, además que como está usando POO lo que podría hacer es separar algunas cosas y que trabajen de forma independiente, o básicamente lo que trato de decir es que en vez de hacer lo anterior, usted podría hacer lo siguiente:

Código: python
while True:
    # Pedimos los datos correspondientes

    try:
        host = input("Host address: ")
        user = input("Username: ")
        passwd = getpass.getpass()

    except (EOFError, KeyboardInterrupt):
        # Para no caer en un bucle infinito, es mejor que el usuario
        # rompa el bucle de una manera más limpia, ya sea pulsando
        # CTRL-C o CTRL-D (en Windows creo que sería CTRL-Z).

        print('Operation cancelled by user :-(')
        break

    # Como se puede apreciar se instancia la clase 'ftpClient' por lo
    # cual estaremos separando funcionalidades, por ende mejora el
    # mantenimiento y usabilidad. Ahora tu clase se podría usar en otro
    # código para un uso diferente o podría usarse directamente en el
    # código como lo estamos haciendo.

    try:
        ftpclient = ftpClient(
            host, user, passwd

        )

    except Exception as err:
        # Es recomendable mostrar la excepción al momento de su ejecución
        # porque puede que el error no sea debido a un error de conexión,
        # puede que sea por un error de código o por cualquier cosa que
        # no podremos saber, lo cual generaría conflictos a la hora de
        # arreglarlo o depurarlo.

        print('Couldn\'t connect to host:', str(err))


El snippet está comentado para que vayas leyendo las recomendaciones y el porqué de su modificación. Ahora vayamos con lo siguiente:

Código: python
str(input("Host address: "))


Si estás usando la versión '2' de python podría funcionar "casi" igual que 'raw_input', no obstante debes abstener todo uso de esa función (le dejaré enlaces que expliquen mi preocupación). Sin embargo python 2 está en desuso por lo que la función 'input' es el nuevo nombre de 'raw_input' (en la versión 3) o básicamente la función encargada de requerir datos por la entrada de usuario.

Prosigamos con la siguiente declaración:

Código: python
os.system("cls")


Parece inofensivo, pero 'cls' es un comando más usado en Windows, MS-DOS, etc; para el caso de Gnu/Linux (por ejemplo), es 'clear', lo cual crearía menos portabilidad, si no es importante para usted, no me haga caso, pero si desea que sea más portable su programa/script/librería/etcétera podría o no usarlo o crear una especie de función que use el comando correspondiente según el Sistema Operativo presente, como por ejemplo:

Código: python
import os

def clear():
    if (os.name == 'nt'):
        # Si la máquina es 'Windows' o similar:

        os.system('cls')

    else:
        os.system('clear')

        # Personalmente me gusta más este "hack" que se explicará
        # mejor con enlaces que dejaré dentro de un rato.
        #
        # El siguiente código sería más eficiente que ejecutar
        # 'os.system("clear")' porque usa códigos 'ANSI' en vez
        # de generar muchísimas llamadas y una bifurcación para
        # ejecutar 'clear', aunque usar estos códigos depende de
        # la plataforma y puede generar uno que otro comportamiento
        # inesperado dependiendo de la situación, aun así es
        # conveniente si no le importa lo que dije xD.
        #
        # print('\033[1J\033[1H', end='')


Si nota, estaremos creando una función llamada "clear" la cual puede ser invocada a conveniencia y también estamos tratando de hacer "portable" el despejar lo escrito en la shell del usuario.

Código: python
        print("""
           
             ___    _____  ___       ___    _                     _   
            (  _`\ (_   _)(  _`\    (  _`\ (_ )  _               ( )_
            | (_(_)  | |  | |_) )   | ( (_) | | (_)   __    ___  | ,_)
            |  _)    | |  | ,__/'   | |  _  | | | | /'__`\/' _ `\| | 
            | |      | |  | |       | (_( ) | | | |(  ___/| ( ) || |_
            (_)      (_)  (_)       (____/'(___)(_)`\____)(_) (_)`\__)

            1. Download Files for FTP server
            2. Upload Files for FTP server
        """)


El uso de triple comillas no es recomendable así nos ahorre tiempo, las triple comillas son usadas para los docstrings.

Código: python
def menu(self):
    ...


y

Código: python
def main(self):
    ...


Esto puede generar confusión a la persona que vaya a hacer mantenimiento o esté programando un programa usando tu librería ya que su cerebro "racional" va a traducir los nombres y los va a asimilar, por lo que va a creer que son iguales o tienen la misma funcionalidad. Lo que se podría hacer es cambiar 'menu' por 'logo' o algo igual de coherente como 'banner'.


Código: python
self.option


No es la única propiedad que defines y usas localmente (por lo que me he dado cuenta), no lo hagas y no es necesario mientras todo sea local. Aunque funcione, imagina que haya que usar 'self.option' (por ejemplo) en alguna parte del código y sea definida al momento de instanciar la clase, el valor sería '1' (o cualquiera, es un ejemplo), entonces tú tranquilamente llamas a un método llamado '.metodo_cualquiera(...)' y por segunda vez llamas a '.otro_metodo(...)', pero de repente ¡pum! ocurre algo inesperado, algo truculento, algo maquiavélico, sientes que el karma, el destino, dios o como lo quieras llamar está en tu contra, pero no, resulta que el primer método al cual llamas modifica la propiedad 'self.option' por un valor que el segundo método no se esperaba. No es necesario que coloques 'self.', al menos que quieras o crear o modificar una propiedad ya existente.


Por cierto, todo lo que te he comentado son recomendaciones y espero te haya ayudado. Te deseo lo mejor y espero sigas aprendiendo, la verdad ya paré de analizar tu código porque los ojos se me están cerrando del cansancio que tengo por trasnocharme xD

Ah, se me olvidaban los enlaces:

Uno: No tienes permitido ver los links. Registrarse o Entrar a mi cuenta
Dos: No tienes permitido ver los links. Registrarse o Entrar a mi cuenta
Tres: No tienes permitido ver los links. Registrarse o Entrar a mi cuenta
Cuatro: No tienes permitido ver los links. Registrarse o Entrar a mi cuenta

~ DtxdF
PGP :: <D82F366940155CB043147178C4E075FC4403BDDC>

~ DtxdF

@No tienes permitido ver los links. Registrarse o Entrar a mi cuenta Agradezco tus consejos enserio, esto me va a ayudar a mejorar bastante. Muchísimas gracias, de verdad :)

Este es el que hice
tiene una funcion .interactivo()   para navegar por el servidor ftp

Código: python

import os
from ftplib import FTP   # clsFTP


import ftputil
import ftputil.session
import time, datetime

my_session_factory = ftputil.session.session_factory(use_passive_mode=False)
class clsFTP:
   
    def __init__(self, strServidor="", strUsuario="", strPassword=""):
        self.bolConectado = False
        self.intReintentos = 3
        if strServidor and strUsuario and strPassword:
            self.conecta(strServidor, strUsuario, strPassword)
   
    def conecta(self, strServidor, strUsuario, strPassword):
        self.strServidor = strServidor
        self.strUsuario = strUsuario
        self.strPassword = strPassword
        try:
            self.ftp = ftputil.FTPHost(strServidor, strUsuario, strPassword, session_factory=my_session_factory )
        except Exception as error:
            print(error)
            self.bolConectado = False
        else:
            print("conexión...")
            self.bolConectado = True
            self.strDirLocal = os.getcwd()
            self.strDirRemoto = self.ftp.getcwd()
            try:
                self.ftp.synchronize_times()
            except Exception as error:
                print("synchronize_times", error)

        return self.bolConectado

    def desconecta(self):
        self.bolConectado = False
        self.ftp.close()


    def dirLocal(self, strDirLocal):
        if not os.path.isdir(strDirLocal):
            os.makedirs(strDirLocal, exist_ok=True)
        os.chdir(strDirLocal) 

    def interactivo(self):
        if not self.bolConectado:
            self.conecta(self.strServidor, self.strUsuario, self.strPassword)
        while 1:
            self.strDirRemoto = self.ftp.getcwd()
            self.strDirLocal = os.getcwd()
            for intR, strlinea  in  enumerate(self.listaRemoto()):
                print(intR, *strlinea)
            print("\nFTP  by UCLA")
            print("Servidor:", self.strServidor)
            print("Directorio Local:",self.strDirLocal)
            print("Directorio Remoto:",self.strDirRemoto)
       
            strOpcion = input("Opción (H ayuda): ").lower().strip()
            if strOpcion == "q":
                break
            elif strOpcion[0:3] == "dir":
                self.listaLocal()
            elif strOpcion[0] == "d":
                intOpcion = int(strOpcion.replace("d",""))
                self.descarga(self.ftp.path.join(self.lstDirRemoto[intOpcion][4], self.lstDirRemoto[intOpcion][0]),
                    os.path.join(self.strDirLocal, self.lstDirRemoto[intOpcion][0]))
            elif strOpcion[0:2] == "..":
                self.ftp.chdir("..")
            elif strOpcion.isdigit():
                intOpcion = int(strOpcion)
                if self.ftp.path.isdir(self.lstDirRemoto[intOpcion][0]):
                    try:
                        self.ftp.chdir(self.lstDirRemoto[intOpcion][0])
                    except Exception as error:
                        print("Cambio de directorio", error)
            elif strOpcion[0:2] == "cd":
                try:
                    os.chdir(strOpcion[3:])
                except Exception as e:
                    print("error al cambiar directorio local " + str(e))
               

    def descarga(self, strRutaArchivoRemoto, strRutaArchivoLocal):
        if self.ftp.path.isfile(strRutaArchivoRemoto):
            for intTry in range(self.intReintentos):
                try:
                    os.makedirs(os.path.dirname(strRutaArchivoLocal), exist_ok=True)
                    print("DESCARGAR ", strRutaArchivoRemoto, " ==> ", os.path.dirname(strRutaArchivoLocal))
                    bolDescargado = self.ftp.download_if_newer(strRutaArchivoRemoto, strRutaArchivoLocal)
                    print(bolDescargado, self.ftp.path.getsize(strRutaArchivoRemoto), os.path.getsize(strRutaArchivoLocal))
                    if self.ftp.path.getsize(strRutaArchivoRemoto) != os.path.getsize(strRutaArchivoLocal):
                        print("tamanos DIFERENTES",self.ftp.path.getsize(strRutaArchivoRemoto),os.path.getsize(strRutaArchivoLocal))
                        input()
                except Exception as error:
                    bolDescargado = False
                    print("ERROR DE DESCARGA!", error)
                    print(f"Reintentando {intTry + 1} de {self.intReintentos}" )
                else:
                    break
        else:

            for strdf in self.ftp.listdir(strRutaArchivoRemoto):
                bolDescargado = self.descarga(self.ftp.path.join(strRutaArchivoRemoto, strdf),os.path.join(strRutaArchivoLocal, strdf))


    def listaLocal(self):
        for elemento in os.listdir():
            if os.path.isdir(elemento):
                print("\t<DIR>", elemento)
            else:
                print("\t", elemento)

    def listaRemoto(self, strRutaRemota = "" ):
        self.lstDirRemoto = []
        if self.bolConectado:
            if strRutaRemota == "":
                strRutaRemota = self.ftp.getcwd()
           
            for df in self.ftp.listdir(strRutaRemota):
                rdf = self.ftp.path.join(strRutaRemota, df)
                strTipo = "<D>" if self.ftp.path.isdir(rdf) else "<F>"
               
                strTamano = "0" if strTipo == "<D>" else "{0:.2f}".format(self.ftp.path.getsize(rdf) / 1024)

                strFecha = time.strftime('%Y-%m-%d', time.gmtime(self.ftp.path.getmtime(rdf)))
                self.lstDirRemoto.append([df, strTipo, strTamano, strFecha, strRutaRemota])
           
        return self.lstDirRemoto
    def Ayuda(self):
        print("\nUn poco de AYUDA:")
        print("""
<numero> - Cambia Directorio Remoto
D <numero> - Descarga Archivo/Directorio Remoto
CD <Directorio> - Cambia Directorio Local
DIR - Listar Directorio Local Actual.
Q - Termina Interactivo\n
        """)