.. developer_tutorial:

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 :py:meth:`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:
:py:meth:`aioftp.Client.download_stream`,
:py:meth:`aioftp.Client.upload_stream`, :py:meth:`aioftp.Client.append_stream`
or (for more complex situations) :py:meth:`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
:py:class:`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
:py:meth:`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.
:py:class:`aioftp.ConnectionConditions`
:py:class:`aioftp.PathConditions`
:py:class:`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 :py:class:`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
:py:class:`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
:py:class:`aioftp.AbstractUserManager` and override it methods. The new user
manager should be passed to server as `users` argument when initialize server.
