W poprzednim poście
opisałem nawiązywanie połączenia z KDEConnect
(przy wykorzystaniu danych z GSConnect).
Teraz czas na przedstawienie, jak działa jedna z najbardziej podstawowych
funkcjonalności tego protokołu - czyli możliwość przesyłania plików. A możemy
je odbierać, jak i wysyłać.
Odbieranie plików
Odbieranie plików inicjuje oczywiście KDEConnect.
Z dostępnych opcji wybieramy “Wysyłanie plików” (pojawi się dialog wyboru
plików, z którego możemy wybrać jeden lub więcej plików do przesłania).
KDEConnect
w takiej sytuacji wysyła do naszego urządzenia pakiet kdeconnect.share.request
zawierający w body informacje o pliku, jak jego nazwa - filename.
W kwestii rozmiaru pliku, korzystamy z payloadSize z poza body.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"id": 1645343489598,
"type": "kdeconnect.share.request",
"body":{
"filename": "Screenshot_2022-02-07-23-30-00-849_com.facebook.lite.jpg",
"lastModified": 1644273001000,
"numberOfFiles": 2,
"totalPayloadSize": 310309
},
"payloadSize": 260509,
"payloadTransferInfo": {
"port": 1739
}
}
|
Transfer pliku nie jest prowadzony w tym samym połączeniu, które przesyła nam
pakiety, ale w nowym połączeniu, które powinniśmy nawiązać z KDEConnect
na porcie podanym w payloadTransferInfo.port (numer portu będzie pomiędzy
1739 a 1764).
W przypadku wysyłania wielu plików - będziemy mieli przesyłane kolejne pakiety
kdeconnect.share.request (następny po zakończeniu pobierania pliku wskazanego
przez poprzedni pakiet). Na wysyłanie wielu plików wskazują nam pola
body.numberOfFiles (ilość plików przesyłanych) i body.totalPayloadSize (suma
wielkości wszystkich plików; payloadSize to wielkość pojedynczego pliku). Np.
następny plik po tym będzie miał następujący pakiet kdeconnect.share.request:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"id": 1645343489646,
"type": "kdeconnect.share.request",
"body":{
"filename": "Screenshot_2022-02-20-08-51-10-065_org.kde.kdeconnect_tp.jpg",
"lastModified": 1645343471000,
"numberOfFiles": 2,
"totalPayloadSize": 310309
},
"payloadSize": 49800,
"payloadTransferInfo": {
"port": 1739
}
}
|
Po teorii - czas na praktykę. W przedstawionym kodzie pyconnect.py
odbieranie pakietu kdeconnect.share.request odbywa się w funkcji
device_connection_task() (odpowiedzialnej po nawiązaniu połączenia, za
przetwarzanie pakietów przychodzących)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
async def device_connection_task(id_packet, remote_ip):
# ...
# receiving packets
bssock = BufferedByteReceiveStream(ssock)
async with anyio.create_task_group() as slave_group:
# ...
while True:
pack_data = await bssock.receive_until(b'\n', 1024 * 1024)
try:
pack = json.loads(pack_data)
except json.JSONDecodeError:
log.exception(
('device_connection_task(): Error while decoding '
f'packet / {pack_data}'))
continue
log.debug(f'device_connection_task(): Packet {pack_data}')
if pack['type'] == 'kdeconnect.share.request':
slave_group.start_soon(download_file_task, pack, remote_ip,
ssl_context)
|
Przy pojawieniu się pakietu z plikiem - przechodzimy do funkcji
download_file_task() (wywoływanej jako oddzielne zadanie). Funkcja ta musi:
- Sprawdzić poprawność pakietu (pola
body.filename, payloadSize i
payloadTransferInfo.port są wymagane). Linie 4 - 12.
- Przygotować nazwę pliku docelowego (linie
14 - 21).
- Nawiązać połączenie z KDEConnect
na porcie wskazanym przez
payloadTransferInfo.port (linie 25 - 27).
- Pobrać plik (zakończenie połączenia nastąpi automatycznie po odebraniu
ostatnich danych). Linie
29 - 39.
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
|
async def download_file_task(pack, remote_ip, ssl_context):
log.info(f'download_file_task() from {remote_ip}')
if 'filename' not in pack['body']:
log.error('download_file_task(): No filename property in pack.')
return
if 'payloadSize' not in pack or 'payloadTransferInfo' not in pack \
or 'port' not in pack['payloadTransferInfo']:
log.error(('download_file_task(): No payloadSize or '
'payloadTransferInfo property in pack.'))
return
# dest filename
filename = os.path.join(os.path.expanduser('~'), pack['body']['filename'])
i = 1
while os.path.exists(filename):
filename = os.path.splitext(pack['body']['filename'])
filename = os.path.join(os.path.expanduser('~'),
f'{filename[0]}-{i}{filename[1]}')
i += 1
log.debug(f'download_file_task(): destination file: {filename}')
# download
async with await anyio.connect_tcp(
remote_ip, pack['payloadTransferInfo']['port'],
ssl_context=ssl_context) as sock:
with open(filename, 'wb') as f:
received = 0
while received < pack['payloadSize']:
data = await sock.receive()
f.write(data)
received += len(data)
print((f'\r* download_file_task(): {pack["body"]["filename"]}'
f' - received bytes +{len(data)} ({received} of'
f' {pack["payloadSize"]})'),
end='')
print('')
log.info('download_file_task(): download connection closed.')
|
Wysyłanie plików
Wysyłanie plików do KDEConnect
przebiega bardzo podobnie do ich odbierania - też korzystamy z pakietu
kdeconnect.share.request, ale to my go układamy i wysyłamy. I my też musimy
(po określeniu, który port w zakresie pomiędzy 1739 a 1764 jest wolny)
otworzyć serwer wysyłania pliku. Jeśli zostanie na nim nawiązane połączenie -
wysyłamy plik i kończymy działanie serwera.
W mojej implementacji pyconnect.py
wykonywane było testowe wysyłanie pliku - uruchamiane jest nowe zadanie
(slave_group.start_soon(test_upload_task, ssock, ssl_context)),
które w dwie sekundy po starcie wysyła plik kodu pyconnect.py na urządzenie.
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
|
async def test_upload_task(ssock, ssl_context):
'''
Test upload task - upload script itself
'''
await anyio.sleep(2)
await upload_file(os.path.abspath(__file__), ssock, ssl_context)
async def upload_file(file_path, ssock, ssl_context):
log.info(f'upload_file(): {file_path=}, {ssock=}, {ssl_context=}')
if not os.path.exists(file_path):
log.error(f'upload_file(): File not exists ({file_path})')
return
# start upload server
file_size = os.path.getsize(file_path)
server = None
close_server_event = anyio.Event()
async def handle_connection(sock_client):
async with sock_client:
with open(file_path, 'rb') as f:
sent = 0
while sent < file_size:
data = f.read(63 * 1024)
await sock_client.send(data)
sent += len(data)
print((f'\rupload_file() sent {sent} of {file_size} '
f'(+{len(data)})'), end='')
print('')
await server.aclose()
close_server_event.set()
transfer_port = 0
for port in range(KDE_CONNECT_TRANSFER_MIN, KDE_CONNECT_TRANSFER_MAX + 1):
try:
server = TLSListener(await anyio.create_tcp_listener(
local_port=port, local_host='0.0.0.0'),
ssl_context=ssl_context, standard_compatible=False)
log.info(f'upload_file(): - Selected port {port}')
transfer_port = port
break
except OSError as e:
if e.errno == 98: # port already in use
continue
raise e
# send ready packet
pack = {
'type': 'kdeconnect.share.request',
'body': {
'filename': os.path.basename(file_path),
'open': False,
'lastModified': int(os.path.getmtime(file_path)),
'numberOfFiles': 1,
'totalPayloadSize': file_size
},
'payloadSize': file_size,
'payloadTransferInfo': {
'port': transfer_port
}
}
serve_forever = server.serve(handle_connection)
await anyio.sleep(0.01)
await ssock.send(prepare_to_send(pack))
log.debug(f'upload_file(): invitation packet sent: {pack!r}')
try:
await serve_forever
except anyio.ClosedResourceError:
close_server_event.set()
await close_server_event.wait()
log.debug('upload_file(): transfer server closed')
|
W następnym poście zajmę się wysyłaniem i odbieraniem SMS-ów.