Developer tutorial

Both, client and server classes are inherit-minded when created. So, you need to inherit class and override and/or add methods to bring your functionality.

Client

For simple commands, which requires no extra connection, realization of new method is pretty simple. You just need to use aioftp.Client.command() (or even don’t use it). For example, lets realize «NOOP» command, which do nothing:

class MyClient(aioftp.Client):
    async def noop(self):
        await self.command("NOOP", "2xx")

Lets take a look to a more complex example. Say, we want to collect some data via extra connection. For this one you need one of «extra connection» methods: aioftp.Client.download_stream(), aioftp.Client.upload_stream(), aioftp.Client.append_stream() or (for more complex situations) aioftp.Client.get_stream() Here we implements some «COLL x» command. I don’t know why, but it retrieve some data via extra connection. And the size of data is equal to «x».

class MyClient(aioftp.Client):

    async def collect(self, count):
        collected = []
        async with self.get_stream("COLL " + str(count), "1xx") as stream:
            async for block in stream.iter_by_block(8):
                i = int.from_bytes(block, "big")
                print("received:", block, i)
                collected.append(i)
        return collected

Client retrieve passive (or active in future versions) via get_stream and read blocks of data until connection is closed. Then finishing stream and return result. Most of client functions (except low-level from BaseClient) are made in pretty same manner. It is a good idea you to see source code of aioftp.Client in client.py to see when and why this or that techniques used.

Server

Server class based on dispatcher, which wait for result of tasks via asyncio.wait(). Tasks are different: command-reader, result-writer, commander-action, extra-connection-workers. FTP methods dispatched by name.

Lets say we want implement «NOOP» command for server again:

class MyServer(aioftp.Server):

    async def noop(self, connection, rest):
        connection.response("200", "boring")
        return True

What we have here? Dispatcher calls our method with some arguments:

  • connection is state of connection, this can hold and wait for futures. There many connection values you can interest in: addresses, throttles, timeouts, extra_workers, response, etc. You can add your own flags and values to the «connection» and edit the existing ones of course. It’s better to see source code of server, cause connection is heart of dispatcher ↔ task and task ↔ task interaction and state container.

  • rest: rest part of command string

There is some decorators, which can help for routine checks: is user logged, can he read/write this path, etc. aioftp.ConnectionConditions aioftp.PathConditions aioftp.PathPermissions

For more complex example lets try same client «COLL x» command.

class MyServer(aioftp.Server):

    @aioftp.ConnectionConditions(
        aioftp.ConnectionConditions.login_required,
        aioftp.ConnectionConditions.passive_server_started)
    async def coll(self, connection, rest):

        @aioftp.ConnectionConditions(
            aioftp.ConnectionConditions.data_connection_made,
            wait=True,
            fail_code="425",
            fail_info="Can't open data connection")
        @aioftp.server.worker
        async def coll_worker(self, connection, rest):
            stream = connection.data_connection
            del connection.data_connection
            async with stream:
                for i in range(count):
                    binary = i.to_bytes(8, "big")
                    await stream.write(binary)
            connection.response("200", "coll transfer done")
            return True

        count = int(rest)
        coro = coll_worker(self, connection, rest)
        task = connection.loop.create_task(coro)
        connection.extra_workers.add(task)
        connection.response("150", "coll transfer started")
        return True

This action requires passive connection, that is why we use worker. We should be able to receive commands when receiving data with extra connection, that is why we send our task to dispatcher via extra_workers. Task will be pending on next «iteration» of dispatcher.

Lets see what we have.

async def test():
    server = MyServer()
    client = MyClient()
    await server.start("127.0.0.1", 8021)
    await client.connect("127.0.0.1", 8021)
    await client.login()
    collected = await client.collect(20)
    print(collected)
    await client.quit()
    await server.close()


if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(name)s] %(message)s",
        datefmt="[%H:%M:%S]:",
    )
    loop = asyncio.get_event_loop()
    loop.run_until_complete(test())
    print("done")

And the output for this is:

[01:18:54]: [aioftp.server] serving on 127.0.0.1:8021
[01:18:54]: [aioftp.server] new connection from 127.0.0.1:48883
[01:18:54]: [aioftp.server] 220 welcome
[01:18:54]: [aioftp.client] 220 welcome
[01:18:54]: [aioftp.client] USER anonymous
[01:18:54]: [aioftp.server] USER anonymous
[01:18:54]: [aioftp.server] 230 anonymous login
[01:18:54]: [aioftp.client] 230 anonymous login
[01:18:54]: [aioftp.client] TYPE I
[01:18:54]: [aioftp.server] TYPE I
[01:18:54]: [aioftp.server] 200
[01:18:54]: [aioftp.client] 200
[01:18:54]: [aioftp.client] PASV
[01:18:54]: [aioftp.server] PASV
[01:18:54]: [aioftp.server] 227-listen socket created
[01:18:54]: [aioftp.server] 227 (127,0,0,1,223,249)
[01:18:54]: [aioftp.client] 227-listen socket created
[01:18:54]: [aioftp.client] 227 (127,0,0,1,223,249)
[01:18:54]: [aioftp.client] COLL 20
[01:18:54]: [aioftp.server] COLL 20
[01:18:54]: [aioftp.server] 150 coll transfer started
[01:18:54]: [aioftp.client] 150 coll transfer started
received: b'\x00\x00\x00\x00\x00\x00\x00\x00' 0
received: b'\x00\x00\x00\x00\x00\x00\x00\x01' 1
received: b'\x00\x00\x00\x00\x00\x00\x00\x02' 2
received: b'\x00\x00\x00\x00\x00\x00\x00\x03' 3
received: b'\x00\x00\x00\x00\x00\x00\x00\x04' 4
received: b'\x00\x00\x00\x00\x00\x00\x00\x05' 5
received: b'\x00\x00\x00\x00\x00\x00\x00\x06' 6
received: b'\x00\x00\x00\x00\x00\x00\x00\x07' 7
received: b'\x00\x00\x00\x00\x00\x00\x00\x08' 8
received: b'\x00\x00\x00\x00\x00\x00\x00\t' 9
received: b'\x00\x00\x00\x00\x00\x00\x00\n' 10
received: b'\x00\x00\x00\x00\x00\x00\x00\x0b' 11
received: b'\x00\x00\x00\x00\x00\x00\x00\x0c' 12
received: b'\x00\x00\x00\x00\x00\x00\x00\r' 13
received: b'\x00\x00\x00\x00\x00\x00\x00\x0e' 14
received: b'\x00\x00\x00\x00\x00\x00\x00\x0f' 15
received: b'\x00\x00\x00\x00\x00\x00\x00\x10' 16
received: b'\x00\x00\x00\x00\x00\x00\x00\x11' 17
received: b'\x00\x00\x00\x00\x00\x00\x00\x12' 18
[01:18:54]: [aioftp.server] 200 coll transfer done
received: b'\x00\x00\x00\x00\x00\x00\x00\x13' 19
[01:18:54]: [aioftp.client] 200 coll transfer done
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[01:18:54]: [aioftp.client] QUIT
[01:18:54]: [aioftp.server] QUIT
[01:18:54]: [aioftp.server] 221 bye
[01:18:54]: [aioftp.server] closing connection from 127.0.0.1:48883
[01:18:54]: [aioftp.client] 221 bye
done

It is a good idea you to see source code of aioftp.Server in server.py to see when and why this or that techniques used.

Path abstraction layer

Since file io is blocking and aioftp tries to be non-blocking ftp library, we need some abstraction layer for filesystem operations. That is why pathio exists. If you want to create your own pathio, then you should inherit aioftp.AbstractPathIO and override it methods.

User Manager

User manager purpose is to split retrieving user information from network or database and server logic. You can create your own user manager by inherit aioftp.AbstractUserManager and override it methods. The new user manager should be passed to server as users argument when initialize server.