File: developer_tutorial.rst

package info (click to toggle)
aioftp 0.26.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 624 kB
  • sloc: python: 5,566; makefile: 172
file content (223 lines) | stat: -rw-r--r-- 8,697 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
.. 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.