librats 1.0.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -331
- package/binding.gyp +16 -3
- package/lib/index.d.ts +288 -696
- package/lib/index.js +407 -44
- package/native-src/3rdparty/android/ifaddrs-android.c +1 -0
- package/native-src/3rdparty/android/ifaddrs-android.h +1 -0
- package/native-src/CMakeLists.txt +404 -179
- package/native-src/LICENSE +1 -1
- package/native-src/src/librats/bindings/rats.cpp +762 -0
- package/native-src/src/librats/bindings/rats.h +380 -0
- package/native-src/src/librats/bittorrent/bencode.cpp +437 -0
- package/native-src/src/librats/bittorrent/bencode.h +176 -0
- package/native-src/src/librats/bittorrent/bitfield.cpp +97 -0
- package/native-src/src/librats/bittorrent/bitfield.h +76 -0
- package/native-src/src/librats/bittorrent/byte_io.h +58 -0
- package/native-src/src/librats/bittorrent/choker.cpp +25 -0
- package/native-src/src/librats/bittorrent/choker.h +46 -0
- package/native-src/src/librats/bittorrent/client.cpp +413 -0
- package/native-src/src/librats/bittorrent/client.h +227 -0
- package/native-src/src/librats/bittorrent/disk_io.cpp +209 -0
- package/native-src/src/librats/bittorrent/disk_io.h +150 -0
- package/native-src/src/librats/bittorrent/extensions.cpp +191 -0
- package/native-src/src/librats/bittorrent/extensions.h +94 -0
- package/native-src/src/librats/bittorrent/file_storage.cpp +77 -0
- package/native-src/src/librats/bittorrent/file_storage.h +79 -0
- package/native-src/src/librats/bittorrent/log.h +42 -0
- package/native-src/src/librats/bittorrent/magnet_uri.cpp +98 -0
- package/native-src/src/librats/bittorrent/magnet_uri.h +35 -0
- package/native-src/src/librats/bittorrent/peer_connection.cpp +502 -0
- package/native-src/src/librats/bittorrent/peer_connection.h +194 -0
- package/native-src/src/librats/bittorrent/peer_list.cpp +68 -0
- package/native-src/src/librats/bittorrent/peer_list.h +75 -0
- package/native-src/src/librats/bittorrent/piece_picker.cpp +352 -0
- package/native-src/src/librats/bittorrent/piece_picker.h +201 -0
- package/native-src/src/librats/bittorrent/reactor.cpp +97 -0
- package/native-src/src/librats/bittorrent/reactor.h +89 -0
- package/native-src/src/librats/bittorrent/resume_data.cpp +72 -0
- package/native-src/src/librats/bittorrent/resume_data.h +41 -0
- package/native-src/src/librats/bittorrent/store_buffer.cpp +48 -0
- package/native-src/src/librats/bittorrent/store_buffer.h +47 -0
- package/native-src/src/librats/bittorrent/torrent.cpp +870 -0
- package/native-src/src/librats/bittorrent/torrent.h +260 -0
- package/native-src/src/librats/bittorrent/torrent_creator.cpp +129 -0
- package/native-src/src/librats/bittorrent/torrent_creator.h +58 -0
- package/native-src/src/librats/bittorrent/torrent_info.cpp +314 -0
- package/native-src/src/librats/bittorrent/torrent_info.h +118 -0
- package/native-src/src/librats/bittorrent/tracker.cpp +374 -0
- package/native-src/src/librats/bittorrent/tracker.h +108 -0
- package/native-src/src/librats/bittorrent/types.cpp +206 -0
- package/native-src/src/librats/bittorrent/types.h +86 -0
- package/native-src/src/librats/core/address.cpp +35 -0
- package/native-src/src/librats/core/address.h +78 -0
- package/native-src/src/librats/core/bytes.h +69 -0
- package/native-src/src/librats/core/chained_send_buffer.cpp +172 -0
- package/native-src/src/librats/core/chained_send_buffer.h +183 -0
- package/native-src/src/librats/core/endpoint_parse.cpp +41 -0
- package/native-src/src/librats/core/endpoint_parse.h +31 -0
- package/native-src/src/librats/core/event_bus.h +70 -0
- package/native-src/src/librats/core/host_endpoint.h +56 -0
- package/native-src/src/{io_poller.cpp → librats/core/io_poller.cpp} +520 -65
- package/native-src/src/{io_poller.h → librats/core/io_poller.h} +12 -6
- package/native-src/src/librats/core/ip_address.cpp +120 -0
- package/native-src/src/librats/core/ip_address.h +109 -0
- package/native-src/src/librats/core/mpsc_queue.h +47 -0
- package/native-src/src/librats/core/notifier.h +74 -0
- package/native-src/src/librats/core/receive_buffer.cpp +219 -0
- package/native-src/src/librats/core/receive_buffer.h +171 -0
- package/native-src/src/librats/core/service_registry.h +58 -0
- package/native-src/src/{socket.cpp → librats/core/socket.cpp} +625 -118
- package/native-src/src/librats/core/socket.h +496 -0
- package/native-src/src/librats/core/timer_queue.h +105 -0
- package/native-src/src/librats/core/types.cpp +43 -0
- package/native-src/src/librats/core/types.h +103 -0
- package/native-src/src/librats/core/wakeup_pipe.h +83 -0
- package/native-src/src/{crypto → librats/crypto}/blake2_endian.h +21 -23
- package/native-src/src/{crypto → librats/crypto}/blake2b.c +34 -33
- package/native-src/src/{crypto → librats/crypto}/blake2b.h +7 -6
- package/native-src/src/{crypto → librats/crypto}/blake2s.c +55 -54
- package/native-src/src/{crypto → librats/crypto}/blake2s.h +13 -12
- package/native-src/src/{crypto → librats/crypto}/chacha.c +22 -21
- package/native-src/src/{crypto → librats/crypto}/chacha.h +14 -13
- package/native-src/src/{crypto → librats/crypto}/chachapoly.c +56 -56
- package/native-src/src/{crypto → librats/crypto}/chachapoly.h +24 -17
- package/native-src/src/{crc32.cpp → librats/crypto/crc32.cpp} +1 -1
- package/native-src/src/{crc32.h → librats/crypto/crc32.h} +3 -1
- package/native-src/src/{crypto → librats/crypto}/curve25519.c +6 -4
- package/native-src/src/{crypto → librats/crypto}/curve25519.h +6 -3
- package/native-src/src/librats/crypto/hkdf.c +266 -0
- package/native-src/src/{crypto → librats/crypto}/hkdf.h +19 -19
- package/native-src/src/{noise.cpp → librats/crypto/noise.cpp} +84 -73
- package/native-src/src/{noise.h → librats/crypto/noise.h} +18 -8
- package/native-src/src/{crypto → librats/crypto}/poly1305.c +47 -46
- package/native-src/src/librats/crypto/poly1305.h +37 -0
- package/native-src/src/{sha1.cpp → librats/crypto/sha1.cpp} +33 -1
- package/native-src/src/{sha1.h → librats/crypto/sha1.h} +14 -6
- package/native-src/src/{crypto → librats/crypto}/sha256.c +15 -14
- package/native-src/src/{crypto → librats/crypto}/sha256.h +8 -7
- package/native-src/src/{crypto → librats/crypto}/sha512.c +15 -14
- package/native-src/src/{crypto → librats/crypto}/sha512.h +8 -7
- package/native-src/src/librats/dht/announce.cpp +37 -0
- package/native-src/src/librats/dht/announce.h +41 -0
- package/native-src/src/librats/dht/bep42.cpp +109 -0
- package/native-src/src/librats/dht/bep42.h +48 -0
- package/native-src/src/librats/dht/dht.cpp +501 -0
- package/native-src/src/librats/dht/dht.h +119 -0
- package/native-src/src/librats/dht/dht_runner.cpp +103 -0
- package/native-src/src/librats/dht/dht_runner.h +71 -0
- package/native-src/src/librats/dht/dos_blocker.cpp +42 -0
- package/native-src/src/librats/dht/dos_blocker.h +47 -0
- package/native-src/src/librats/dht/find_peers.cpp +52 -0
- package/native-src/src/librats/dht/find_peers.h +73 -0
- package/native-src/src/librats/dht/id.h +167 -0
- package/native-src/src/{krpc.cpp → librats/dht/krpc.cpp} +32 -81
- package/native-src/src/{krpc.h → librats/dht/krpc.h} +19 -23
- package/native-src/src/librats/dht/log.h +38 -0
- package/native-src/src/librats/dht/node.cpp +473 -0
- package/native-src/src/librats/dht/node.h +164 -0
- package/native-src/src/librats/dht/node_entry.h +81 -0
- package/native-src/src/librats/dht/observer.h +72 -0
- package/native-src/src/librats/dht/persistence.cpp +90 -0
- package/native-src/src/librats/dht/persistence.h +32 -0
- package/native-src/src/librats/dht/routing_table.cpp +559 -0
- package/native-src/src/librats/dht/routing_table.h +185 -0
- package/native-src/src/librats/dht/rpc_manager.cpp +127 -0
- package/native-src/src/librats/dht/rpc_manager.h +77 -0
- package/native-src/src/librats/dht/storage.cpp +92 -0
- package/native-src/src/librats/dht/storage.h +74 -0
- package/native-src/src/librats/dht/transport.h +27 -0
- package/native-src/src/librats/dht/traversal.cpp +326 -0
- package/native-src/src/librats/dht/traversal.h +120 -0
- package/native-src/src/librats/dht/udp_transport.cpp +49 -0
- package/native-src/src/librats/dht/udp_transport.h +51 -0
- package/native-src/src/librats/mdns/log.h +22 -0
- package/native-src/src/{mdns.cpp → librats/mdns/mdns.cpp} +75 -40
- package/native-src/src/{mdns.h → librats/mdns/mdns.h} +9 -8
- package/native-src/src/{natpmp.cpp → librats/nat/natpmp.cpp} +12 -9
- package/native-src/src/{natpmp.h → librats/nat/natpmp.h} +3 -3
- package/native-src/src/{port_mapping.h → librats/nat/port_mapping.h} +3 -2
- package/native-src/src/{stun.cpp → librats/nat/stun.cpp} +4 -4
- package/native-src/src/{stun.h → librats/nat/stun.h} +1 -1
- package/native-src/src/{upnp.cpp → librats/nat/upnp.cpp} +6 -6
- package/native-src/src/{upnp.h → librats/nat/upnp.h} +2 -2
- package/native-src/src/librats/node/circuit_service.h +84 -0
- package/native-src/src/librats/node/config.h +110 -0
- package/native-src/src/librats/node/dial_service.h +54 -0
- package/native-src/src/librats/node/dialer.cpp +264 -0
- package/native-src/src/librats/node/dialer.h +188 -0
- package/native-src/src/librats/node/host_events.h +26 -0
- package/native-src/src/librats/node/identify.cpp +130 -0
- package/native-src/src/librats/node/identify.h +71 -0
- package/native-src/src/librats/node/nat_status.cpp +103 -0
- package/native-src/src/librats/node/nat_status.h +118 -0
- package/native-src/src/librats/node/node.cpp +865 -0
- package/native-src/src/librats/node/node.h +344 -0
- package/native-src/src/librats/node/node_context.h +33 -0
- package/native-src/src/librats/node/peer_network.h +91 -0
- package/native-src/src/librats/peer/peer.h +49 -0
- package/native-src/src/librats/peer/peer_book.cpp +181 -0
- package/native-src/src/librats/peer/peer_book.h +88 -0
- package/native-src/src/librats/peer/peer_id.cpp +72 -0
- package/native-src/src/librats/peer/peer_id.h +62 -0
- package/native-src/src/librats/peer/peer_info.h +37 -0
- package/native-src/src/librats/peer/peer_table.cpp +170 -0
- package/native-src/src/librats/peer/peer_table.h +148 -0
- package/native-src/src/librats/security/handshaker.h +66 -0
- package/native-src/src/librats/security/identity.h +43 -0
- package/native-src/src/librats/security/noise_security.cpp +122 -0
- package/native-src/src/librats/security/noise_security.h +37 -0
- package/native-src/src/librats/security/plaintext_security.h +106 -0
- package/native-src/src/librats/security/session.h +37 -0
- package/native-src/src/{storage.cpp → librats/storage/storage.cpp} +369 -522
- package/native-src/src/{storage.h → librats/storage/storage.h} +135 -299
- package/native-src/src/librats/subsystems/bittorrent.cpp +211 -0
- package/native-src/src/librats/subsystems/bittorrent.h +136 -0
- package/native-src/src/librats/subsystems/dht_discovery.cpp +202 -0
- package/native-src/src/librats/subsystems/dht_discovery.h +123 -0
- package/native-src/src/librats/subsystems/dht_service.h +36 -0
- package/native-src/src/librats/subsystems/file_transfer.cpp +972 -0
- package/native-src/src/librats/subsystems/file_transfer.h +367 -0
- package/native-src/src/librats/subsystems/hole_punch.cpp +605 -0
- package/native-src/src/librats/subsystems/hole_punch.h +290 -0
- package/native-src/src/librats/subsystems/hole_punch_service.h +38 -0
- package/native-src/src/librats/subsystems/mdns_discovery.cpp +66 -0
- package/native-src/src/librats/subsystems/mdns_discovery.h +55 -0
- package/native-src/src/librats/subsystems/message_json.cpp +112 -0
- package/native-src/src/librats/subsystems/message_json.h +88 -0
- package/native-src/src/librats/subsystems/peer_exchange.cpp +241 -0
- package/native-src/src/librats/subsystems/peer_exchange.h +136 -0
- package/native-src/src/librats/subsystems/ping_service.cpp +98 -0
- package/native-src/src/librats/subsystems/ping_service.h +66 -0
- package/native-src/src/librats/subsystems/port_mapping_service.cpp +192 -0
- package/native-src/src/librats/subsystems/port_mapping_service.h +84 -0
- package/native-src/src/librats/subsystems/pubsub.cpp +567 -0
- package/native-src/src/librats/subsystems/pubsub.h +175 -0
- package/native-src/src/librats/subsystems/reconnection.cpp +239 -0
- package/native-src/src/librats/subsystems/reconnection.h +126 -0
- package/native-src/src/librats/subsystems/relay.cpp +1142 -0
- package/native-src/src/librats/subsystems/relay.h +211 -0
- package/native-src/src/librats/subsystems/relay_service.h +46 -0
- package/native-src/src/librats/transport/connection.cpp +343 -0
- package/native-src/src/librats/transport/connection.h +283 -0
- package/native-src/src/librats/transport/link.h +96 -0
- package/native-src/src/librats/transport/reactor.cpp +588 -0
- package/native-src/src/librats/transport/reactor.h +262 -0
- package/native-src/src/librats/transport/reactor_pool.h +81 -0
- package/native-src/src/librats/transport/relay_link.cpp +208 -0
- package/native-src/src/librats/transport/relay_link.h +303 -0
- package/native-src/src/librats/transport/tcp_link.cpp +49 -0
- package/native-src/src/librats/transport/tcp_link.h +43 -0
- package/native-src/src/librats/transport/udp_mux.cpp +617 -0
- package/native-src/src/librats/transport/udp_mux.h +363 -0
- package/native-src/src/librats/transport/udp_packet.cpp +121 -0
- package/native-src/src/librats/transport/udp_packet.h +190 -0
- package/native-src/src/librats/transport/udp_stream.cpp +1194 -0
- package/native-src/src/librats/transport/udp_stream.h +614 -0
- package/native-src/src/librats/util/features.h.in +51 -0
- package/native-src/src/{fs.cpp → librats/util/fs.cpp} +51 -3
- package/native-src/src/librats/util/fs.h +136 -0
- package/native-src/src/librats/util/json.cpp +1002 -0
- package/native-src/src/librats/util/json.h +444 -0
- package/native-src/src/{logger.cpp → librats/util/logger.cpp} +1 -1
- package/native-src/src/{logger.h → librats/util/logger.h} +43 -31
- package/native-src/src/{network_monitor.cpp → librats/util/network_monitor.cpp} +12 -4
- package/native-src/src/{network_monitor.h → librats/util/network_monitor.h} +2 -1
- package/native-src/src/{network_utils.cpp → librats/util/network_utils.cpp} +38 -23
- package/native-src/src/{network_utils.h → librats/util/network_utils.h} +15 -8
- package/native-src/src/{os.cpp → librats/util/os.cpp} +48 -18
- package/native-src/src/librats/util/rats_export.h +69 -0
- package/native-src/src/{version.cpp → librats/util/version.cpp} +2 -2
- package/native-src/src/{version.h.in → librats/util/version.h.in} +1 -1
- package/native-src/src/librats/wire/frame.cpp +76 -0
- package/native-src/src/librats/wire/frame.h +111 -0
- package/native-src/src/librats/wire/message_router.cpp +45 -0
- package/native-src/src/librats/wire/message_router.h +47 -0
- package/package.json +5 -4
- package/scripts/build-librats.js +1 -0
- package/scripts/postinstall.js +3 -3
- package/scripts/prepare-package.js +4 -4
- package/scripts/verify-installation.js +63 -105
- package/src/librats_node.cpp +1067 -1323
- package/native-src/src/bencode.cpp +0 -485
- package/native-src/src/bencode.h +0 -145
- package/native-src/src/bittorrent.cpp +0 -14
- package/native-src/src/bittorrent.h +0 -74
- package/native-src/src/bt_bitfield.cpp +0 -372
- package/native-src/src/bt_bitfield.h +0 -316
- package/native-src/src/bt_choker.cpp +0 -228
- package/native-src/src/bt_choker.h +0 -147
- package/native-src/src/bt_client.cpp +0 -1047
- package/native-src/src/bt_client.h +0 -445
- package/native-src/src/bt_create_torrent.cpp +0 -677
- package/native-src/src/bt_create_torrent.h +0 -473
- package/native-src/src/bt_extension.cpp +0 -469
- package/native-src/src/bt_extension.h +0 -309
- package/native-src/src/bt_file_storage.cpp +0 -261
- package/native-src/src/bt_file_storage.h +0 -298
- package/native-src/src/bt_handshake.cpp +0 -134
- package/native-src/src/bt_handshake.h +0 -157
- package/native-src/src/bt_messages.cpp +0 -364
- package/native-src/src/bt_messages.h +0 -324
- package/native-src/src/bt_network.cpp +0 -1007
- package/native-src/src/bt_network.h +0 -417
- package/native-src/src/bt_peer_connection.cpp +0 -742
- package/native-src/src/bt_peer_connection.h +0 -592
- package/native-src/src/bt_piece_picker.cpp +0 -786
- package/native-src/src/bt_piece_picker.h +0 -473
- package/native-src/src/bt_resume_data.cpp +0 -410
- package/native-src/src/bt_resume_data.h +0 -249
- package/native-src/src/bt_torrent.cpp +0 -2120
- package/native-src/src/bt_torrent.h +0 -641
- package/native-src/src/bt_torrent_info.cpp +0 -659
- package/native-src/src/bt_torrent_info.h +0 -418
- package/native-src/src/bt_types.h +0 -621
- package/native-src/src/chained_send_buffer.cpp +0 -75
- package/native-src/src/chained_send_buffer.h +0 -137
- package/native-src/src/crypto/hkdf.c +0 -266
- package/native-src/src/crypto/poly1305.h +0 -36
- package/native-src/src/dht.cpp +0 -3311
- package/native-src/src/dht.h +0 -717
- package/native-src/src/disk_io.cpp +0 -632
- package/native-src/src/disk_io.h +0 -315
- package/native-src/src/file_transfer.cpp +0 -1415
- package/native-src/src/file_transfer.h +0 -286
- package/native-src/src/fs.h +0 -108
- package/native-src/src/gossipsub.cpp +0 -1139
- package/native-src/src/gossipsub.h +0 -403
- package/native-src/src/ice.cpp +0 -893
- package/native-src/src/ice.h +0 -559
- package/native-src/src/json.hpp +0 -25526
- package/native-src/src/librats.cpp +0 -2378
- package/native-src/src/librats.h +0 -2324
- package/native-src/src/librats_bittorrent.cpp +0 -601
- package/native-src/src/librats_c.cpp +0 -1557
- package/native-src/src/librats_c.h +0 -323
- package/native-src/src/librats_discovery.cpp +0 -402
- package/native-src/src/librats_encryption.cpp +0 -275
- package/native-src/src/librats_file_transfer.cpp +0 -144
- package/native-src/src/librats_gossipsub.cpp +0 -289
- package/native-src/src/librats_ice.cpp +0 -213
- package/native-src/src/librats_log_macros.h +0 -36
- package/native-src/src/librats_logging.cpp +0 -173
- package/native-src/src/librats_mdns.cpp +0 -166
- package/native-src/src/librats_persistence.cpp +0 -796
- package/native-src/src/librats_portmap.cpp +0 -419
- package/native-src/src/librats_reconnection.cpp +0 -218
- package/native-src/src/librats_statistic.cpp +0 -105
- package/native-src/src/librats_storage.cpp +0 -189
- package/native-src/src/rats_export.h +0 -17
- package/native-src/src/receive_buffer.cpp +0 -82
- package/native-src/src/receive_buffer.h +0 -127
- package/native-src/src/socket.h +0 -228
- package/native-src/src/threadmanager.cpp +0 -105
- package/native-src/src/threadmanager.h +0 -53
- package/native-src/src/tracker.cpp +0 -1264
- package/native-src/src/tracker.h +0 -319
- package/native-src/src/turn.cpp +0 -762
- package/native-src/src/turn.h +0 -460
- package/native-src/src/wakeup_pipe.h +0 -60
- /package/native-src/src/{os.h → librats/util/os.h} +0 -0
|
@@ -1,2120 +0,0 @@
|
|
|
1
|
-
#include "bt_torrent.h"
|
|
2
|
-
#include "bt_extension.h"
|
|
3
|
-
#include "bt_resume_data.h"
|
|
4
|
-
#include "bencode.h"
|
|
5
|
-
#include "disk_io.h"
|
|
6
|
-
#include "logger.h"
|
|
7
|
-
#include "fs.h"
|
|
8
|
-
#include "sha1.h"
|
|
9
|
-
#include <algorithm>
|
|
10
|
-
#include <cctype>
|
|
11
|
-
#include <cstring>
|
|
12
|
-
#include <thread>
|
|
13
|
-
#include <ctime>
|
|
14
|
-
|
|
15
|
-
namespace librats {
|
|
16
|
-
|
|
17
|
-
//=============================================================================
|
|
18
|
-
// Helpers
|
|
19
|
-
//=============================================================================
|
|
20
|
-
|
|
21
|
-
const char* torrent_state_to_string(TorrentState state) {
|
|
22
|
-
switch (state) {
|
|
23
|
-
case TorrentState::Stopped: return "Stopped";
|
|
24
|
-
case TorrentState::CheckingFiles: return "CheckingFiles";
|
|
25
|
-
case TorrentState::DownloadingMetadata: return "DownloadingMetadata";
|
|
26
|
-
case TorrentState::Downloading: return "Downloading";
|
|
27
|
-
case TorrentState::Seeding: return "Seeding";
|
|
28
|
-
case TorrentState::Paused: return "Paused";
|
|
29
|
-
case TorrentState::Error: return "Error";
|
|
30
|
-
default: return "Unknown";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Format bytes to human-readable string (KB, MB, GB)
|
|
35
|
-
static std::string format_size(uint64_t bytes) {
|
|
36
|
-
const char* units[] = {"B", "KB", "MB", "GB", "TB"};
|
|
37
|
-
int unit_idx = 0;
|
|
38
|
-
double size = static_cast<double>(bytes);
|
|
39
|
-
|
|
40
|
-
while (size >= 1024.0 && unit_idx < 4) {
|
|
41
|
-
size /= 1024.0;
|
|
42
|
-
unit_idx++;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
char buf[32];
|
|
46
|
-
if (unit_idx == 0) {
|
|
47
|
-
snprintf(buf, sizeof(buf), "%.0f %s", size, units[unit_idx]);
|
|
48
|
-
} else {
|
|
49
|
-
snprintf(buf, sizeof(buf), "%.2f %s", size, units[unit_idx]);
|
|
50
|
-
}
|
|
51
|
-
return buf;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Format speed to human-readable string (KB/s, MB/s)
|
|
55
|
-
static std::string format_speed(uint64_t bytes_per_sec) {
|
|
56
|
-
return format_size(bytes_per_sec) + "/s";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Format ETA to human-readable string
|
|
60
|
-
static std::string format_eta(std::chrono::seconds eta) {
|
|
61
|
-
if (eta.count() <= 0) return "∞";
|
|
62
|
-
|
|
63
|
-
auto total_secs = eta.count();
|
|
64
|
-
int hours = static_cast<int>(total_secs / 3600);
|
|
65
|
-
int mins = static_cast<int>((total_secs % 3600) / 60);
|
|
66
|
-
int secs = static_cast<int>(total_secs % 60);
|
|
67
|
-
|
|
68
|
-
char buf[32];
|
|
69
|
-
if (hours > 0) {
|
|
70
|
-
snprintf(buf, sizeof(buf), "%dh %dm %ds", hours, mins, secs);
|
|
71
|
-
} else if (mins > 0) {
|
|
72
|
-
snprintf(buf, sizeof(buf), "%dm %ds", mins, secs);
|
|
73
|
-
} else {
|
|
74
|
-
snprintf(buf, sizeof(buf), "%ds", secs);
|
|
75
|
-
}
|
|
76
|
-
return buf;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
//=============================================================================
|
|
80
|
-
// Constructor / Destructor
|
|
81
|
-
//=============================================================================
|
|
82
|
-
|
|
83
|
-
Torrent::Torrent(const TorrentInfo& info,
|
|
84
|
-
const TorrentConfig& config,
|
|
85
|
-
const PeerID& our_peer_id)
|
|
86
|
-
: info_hash_(info.info_hash())
|
|
87
|
-
, name_(info.name())
|
|
88
|
-
, our_peer_id_(our_peer_id)
|
|
89
|
-
, info_(std::make_unique<TorrentInfo>(info))
|
|
90
|
-
, config_(config)
|
|
91
|
-
, state_(TorrentState::Stopped)
|
|
92
|
-
, have_pieces_(info.num_pieces()) {
|
|
93
|
-
|
|
94
|
-
LOG_INFO("Torrent", "Creating torrent '" + name_ + "' with " +
|
|
95
|
-
std::to_string(info.num_pieces()) + " pieces, total size=" +
|
|
96
|
-
std::to_string(info.total_size()) + " bytes");
|
|
97
|
-
|
|
98
|
-
// Initialize piece picker
|
|
99
|
-
uint32_t last_piece_len = info.piece_size(info.num_pieces() - 1);
|
|
100
|
-
picker_ = std::make_unique<PiecePicker>(
|
|
101
|
-
info.num_pieces(),
|
|
102
|
-
info.piece_length(),
|
|
103
|
-
last_piece_len
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
if (config_.sequential_download) {
|
|
107
|
-
picker_->set_mode(PickerMode::Sequential);
|
|
108
|
-
LOG_DEBUG("Torrent", "Sequential download mode enabled");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Configure choker
|
|
112
|
-
ChokerConfig choker_config;
|
|
113
|
-
choker_config.max_uploads = config_.max_uploads;
|
|
114
|
-
choker_.set_config(choker_config);
|
|
115
|
-
|
|
116
|
-
stats_.total_size = info.total_size();
|
|
117
|
-
stats_.pieces_total = info.num_pieces();
|
|
118
|
-
|
|
119
|
-
// Initialize time tracking
|
|
120
|
-
added_time_ = std::time(nullptr);
|
|
121
|
-
last_activity_check_ = std::chrono::steady_clock::now();
|
|
122
|
-
|
|
123
|
-
LOG_DEBUG("Torrent", "Torrent '" + name_ + "' initialized successfully");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
Torrent::Torrent(const BtInfoHash& info_hash,
|
|
127
|
-
const std::string& name,
|
|
128
|
-
const TorrentConfig& config,
|
|
129
|
-
const PeerID& our_peer_id)
|
|
130
|
-
: info_hash_(info_hash)
|
|
131
|
-
, name_(name)
|
|
132
|
-
, our_peer_id_(our_peer_id)
|
|
133
|
-
, config_(config)
|
|
134
|
-
, state_(TorrentState::Stopped)
|
|
135
|
-
, have_pieces_(0) {
|
|
136
|
-
|
|
137
|
-
LOG_INFO("Torrent", "Creating torrent '" + name + "' from magnet link (no metadata yet), info_hash=" +
|
|
138
|
-
info_hash_to_hex(info_hash));
|
|
139
|
-
|
|
140
|
-
// No picker until we have metadata
|
|
141
|
-
|
|
142
|
-
ChokerConfig choker_config;
|
|
143
|
-
choker_config.max_uploads = config_.max_uploads;
|
|
144
|
-
choker_.set_config(choker_config);
|
|
145
|
-
|
|
146
|
-
// Initialize time tracking
|
|
147
|
-
added_time_ = std::time(nullptr);
|
|
148
|
-
last_activity_check_ = std::chrono::steady_clock::now();
|
|
149
|
-
|
|
150
|
-
LOG_DEBUG("Torrent", "Torrent '" + name + "' initialized (awaiting metadata)");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
Torrent::~Torrent() {
|
|
154
|
-
LOG_DEBUG("Torrent", "Destroying torrent '" + name_ + "'");
|
|
155
|
-
stop();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
//=============================================================================
|
|
159
|
-
// Lifecycle
|
|
160
|
-
//=============================================================================
|
|
161
|
-
|
|
162
|
-
void Torrent::start() {
|
|
163
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
164
|
-
|
|
165
|
-
LOG_INFO("Torrent", "Starting torrent '" + name_ + "'");
|
|
166
|
-
|
|
167
|
-
if (state_ == TorrentState::Downloading ||
|
|
168
|
-
state_ == TorrentState::Seeding) {
|
|
169
|
-
LOG_DEBUG("Torrent", "Torrent '" + name_ + "' already active, ignoring start()");
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
stats_.started_at = std::chrono::steady_clock::now();
|
|
174
|
-
last_stats_update_ = std::chrono::steady_clock::now();
|
|
175
|
-
last_choker_run_ = std::chrono::steady_clock::now();
|
|
176
|
-
|
|
177
|
-
if (!info_ || !info_->has_metadata()) {
|
|
178
|
-
// No metadata yet - need to download it from peers
|
|
179
|
-
LOG_INFO("Torrent", "No metadata available, starting metadata download for '" + name_ + "'");
|
|
180
|
-
metadata_download_started_ = std::chrono::steady_clock::now();
|
|
181
|
-
set_state(TorrentState::DownloadingMetadata);
|
|
182
|
-
} else if (config_.seed_mode) {
|
|
183
|
-
// Seed mode: assume all files are complete, mark all pieces as "have"
|
|
184
|
-
LOG_INFO("Torrent", "Torrent '" + name_ + "' in seed mode, marking all pieces as have");
|
|
185
|
-
|
|
186
|
-
// Mark all pieces as have in the picker
|
|
187
|
-
if (picker_) {
|
|
188
|
-
picker_->mark_have_all();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Also set our have_pieces bitfield
|
|
192
|
-
have_pieces_ = Bitfield(info_->num_pieces());
|
|
193
|
-
have_pieces_.set_all();
|
|
194
|
-
|
|
195
|
-
// Update stats
|
|
196
|
-
stats_.pieces_done = info_->num_pieces();
|
|
197
|
-
stats_.bytes_done = info_->total_size();
|
|
198
|
-
stats_.progress = 1.0f;
|
|
199
|
-
|
|
200
|
-
set_state(TorrentState::Seeding);
|
|
201
|
-
choker_.set_seed_mode(true);
|
|
202
|
-
|
|
203
|
-
LOG_INFO("Torrent", "Torrent '" + name_ + "' seeding with " +
|
|
204
|
-
std::to_string(info_->num_pieces()) + " pieces");
|
|
205
|
-
} else if (is_complete_unlocked()) {
|
|
206
|
-
LOG_INFO("Torrent", "Torrent '" + name_ + "' is complete, entering seeding mode");
|
|
207
|
-
set_state(TorrentState::Seeding);
|
|
208
|
-
choker_.set_seed_mode(true);
|
|
209
|
-
} else {
|
|
210
|
-
LOG_INFO("Torrent", "Starting download for '" + name_ + "'");
|
|
211
|
-
set_state(TorrentState::Downloading);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
void Torrent::stop() {
|
|
216
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
217
|
-
|
|
218
|
-
LOG_INFO("Torrent", "Stopping torrent '" + name_ + "'");
|
|
219
|
-
|
|
220
|
-
// Disconnect all peers
|
|
221
|
-
size_t num_connections = connections_.size();
|
|
222
|
-
for (auto& conn : connections_) {
|
|
223
|
-
conn->close();
|
|
224
|
-
}
|
|
225
|
-
connections_.clear();
|
|
226
|
-
LOG_DEBUG("Torrent", "Disconnected " + std::to_string(num_connections) + " peers");
|
|
227
|
-
|
|
228
|
-
// Clear pending peers
|
|
229
|
-
size_t num_pending = pending_peers_.size();
|
|
230
|
-
pending_peers_.clear();
|
|
231
|
-
LOG_DEBUG("Torrent", "Cleared " + std::to_string(num_pending) + " pending peers");
|
|
232
|
-
|
|
233
|
-
// Clear metadata exchange state
|
|
234
|
-
metadata_buffer_.clear();
|
|
235
|
-
metadata_pieces_received_.clear();
|
|
236
|
-
metadata_pieces_requested_.clear();
|
|
237
|
-
peer_metadata_size_.clear();
|
|
238
|
-
peer_ut_metadata_id_.clear();
|
|
239
|
-
|
|
240
|
-
set_state(TorrentState::Stopped);
|
|
241
|
-
|
|
242
|
-
// Clear callbacks to prevent dangling pointer issues after stop
|
|
243
|
-
// Note: Keeping on_state_change_ until after set_state() call above
|
|
244
|
-
on_state_change_ = nullptr;
|
|
245
|
-
on_piece_complete_ = nullptr;
|
|
246
|
-
on_complete_ = nullptr;
|
|
247
|
-
on_error_ = nullptr;
|
|
248
|
-
on_metadata_received_ = nullptr;
|
|
249
|
-
|
|
250
|
-
// Extended callbacks (rats-search compatibility)
|
|
251
|
-
on_progress_ = nullptr;
|
|
252
|
-
on_torrent_complete_ = nullptr;
|
|
253
|
-
on_metadata_complete_ = nullptr;
|
|
254
|
-
on_peer_connected_ext_ = nullptr;
|
|
255
|
-
on_peer_disconnected_ext_ = nullptr;
|
|
256
|
-
|
|
257
|
-
LOG_INFO("Torrent", "Torrent '" + name_ + "' stopped");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
void Torrent::pause() {
|
|
261
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
262
|
-
|
|
263
|
-
if (state_ == TorrentState::Downloading ||
|
|
264
|
-
state_ == TorrentState::Seeding) {
|
|
265
|
-
LOG_INFO("Torrent", "Pausing torrent '" + name_ + "' (was " +
|
|
266
|
-
torrent_state_to_string(state_) + ")");
|
|
267
|
-
set_state(TorrentState::Paused);
|
|
268
|
-
} else {
|
|
269
|
-
LOG_DEBUG("Torrent", "Cannot pause torrent '" + name_ + "' in state " +
|
|
270
|
-
torrent_state_to_string(state_));
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
void Torrent::resume() {
|
|
275
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
276
|
-
|
|
277
|
-
if (state_ == TorrentState::Paused) {
|
|
278
|
-
if (is_complete_unlocked()) {
|
|
279
|
-
LOG_INFO("Torrent", "Resuming torrent '" + name_ + "' in seeding mode");
|
|
280
|
-
set_state(TorrentState::Seeding);
|
|
281
|
-
} else {
|
|
282
|
-
LOG_INFO("Torrent", "Resuming torrent '" + name_ + "' in downloading mode");
|
|
283
|
-
set_state(TorrentState::Downloading);
|
|
284
|
-
}
|
|
285
|
-
} else {
|
|
286
|
-
LOG_DEBUG("Torrent", "Cannot resume torrent '" + name_ + "' in state " +
|
|
287
|
-
torrent_state_to_string(state_));
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
void Torrent::recheck() {
|
|
292
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
293
|
-
|
|
294
|
-
LOG_INFO("Torrent", "Starting recheck for torrent '" + name_ + "'");
|
|
295
|
-
|
|
296
|
-
// Would trigger file checking
|
|
297
|
-
set_state(TorrentState::CheckingFiles);
|
|
298
|
-
|
|
299
|
-
// Reset have bitfield
|
|
300
|
-
if (info_) {
|
|
301
|
-
have_pieces_ = Bitfield(info_->num_pieces());
|
|
302
|
-
picker_->set_have_bitfield(have_pieces_);
|
|
303
|
-
LOG_DEBUG("Torrent", "Reset bitfield for " + std::to_string(info_->num_pieces()) + " pieces");
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// TODO: Actually verify files
|
|
307
|
-
LOG_WARN("Torrent", "File verification not yet implemented");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
//=============================================================================
|
|
311
|
-
// State
|
|
312
|
-
//=============================================================================
|
|
313
|
-
|
|
314
|
-
bool Torrent::is_active() const {
|
|
315
|
-
TorrentState s = state_;
|
|
316
|
-
return s == TorrentState::Downloading ||
|
|
317
|
-
s == TorrentState::Seeding ||
|
|
318
|
-
s == TorrentState::DownloadingMetadata;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
bool Torrent::is_complete() const {
|
|
322
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
323
|
-
return is_complete_unlocked();
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
bool Torrent::is_complete_unlocked() const {
|
|
327
|
-
return picker_ && picker_->is_complete();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
bool Torrent::has_metadata() const {
|
|
331
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
332
|
-
return has_metadata_unlocked();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
bool Torrent::has_metadata_unlocked() const {
|
|
336
|
-
return info_ != nullptr && info_->has_metadata();
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
//=============================================================================
|
|
340
|
-
// Accessor Methods (rats-search compatibility)
|
|
341
|
-
//=============================================================================
|
|
342
|
-
|
|
343
|
-
// Static empty TorrentInfo for when metadata is not available
|
|
344
|
-
static TorrentInfo s_empty_torrent_info;
|
|
345
|
-
|
|
346
|
-
const TorrentInfo& Torrent::get_torrent_info() const {
|
|
347
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
348
|
-
if (info_) {
|
|
349
|
-
return *info_;
|
|
350
|
-
}
|
|
351
|
-
return s_empty_torrent_info;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
uint64_t Torrent::downloaded_bytes() const {
|
|
355
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
356
|
-
return stats_.bytes_done;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
double Torrent::progress_percentage() const {
|
|
360
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
361
|
-
return stats_.progress * 100.0;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
double Torrent::download_speed() const {
|
|
365
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
366
|
-
return static_cast<double>(stats_.download_rate);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
void Torrent::set_state(TorrentState new_state) {
|
|
370
|
-
TorrentState old_state = state_.exchange(new_state);
|
|
371
|
-
if (old_state != new_state) {
|
|
372
|
-
LOG_DEBUG("Torrent", "State change: " + std::string(torrent_state_to_string(old_state)) +
|
|
373
|
-
" -> " + std::string(torrent_state_to_string(new_state)) +
|
|
374
|
-
" for '" + name_ + "'");
|
|
375
|
-
if (on_state_change_) {
|
|
376
|
-
on_state_change_(this, new_state);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
//=============================================================================
|
|
382
|
-
// Statistics
|
|
383
|
-
//=============================================================================
|
|
384
|
-
|
|
385
|
-
TorrentStats Torrent::stats() const {
|
|
386
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
387
|
-
return stats_;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
Bitfield Torrent::get_have_bitfield() const {
|
|
391
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
392
|
-
if (picker_) {
|
|
393
|
-
return picker_->get_have_bitfield();
|
|
394
|
-
}
|
|
395
|
-
return have_pieces_;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
//=============================================================================
|
|
399
|
-
// Peer Management
|
|
400
|
-
//=============================================================================
|
|
401
|
-
|
|
402
|
-
void Torrent::add_peer(const std::string& ip, uint16_t port) {
|
|
403
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
404
|
-
|
|
405
|
-
// Check if already connected
|
|
406
|
-
for (const auto& conn : connections_) {
|
|
407
|
-
if (conn->ip() == ip && conn->port() == port) {
|
|
408
|
-
LOG_DEBUG("Torrent", "Peer " + ip + ":" + std::to_string(port) + " already connected");
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Check if already pending
|
|
414
|
-
for (const auto& peer : pending_peers_) {
|
|
415
|
-
if (peer.first == ip && peer.second == port) {
|
|
416
|
-
LOG_DEBUG("Torrent", "Peer " + ip + ":" + std::to_string(port) + " already pending");
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
pending_peers_.emplace_back(ip, port);
|
|
422
|
-
LOG_DEBUG("Torrent", "Added pending peer " + ip + ":" + std::to_string(port) +
|
|
423
|
-
" (total pending=" + std::to_string(pending_peers_.size()) + ")");
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
void Torrent::add_peers(const std::vector<std::pair<std::string, uint16_t>>& peers) {
|
|
427
|
-
for (const auto& peer : peers) {
|
|
428
|
-
add_peer(peer.first, peer.second);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
size_t Torrent::num_peers() const {
|
|
433
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
434
|
-
return connections_.size();
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
std::vector<BtPeerConnection*> Torrent::peers() const {
|
|
438
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
439
|
-
std::vector<BtPeerConnection*> result;
|
|
440
|
-
result.reserve(connections_.size());
|
|
441
|
-
for (const auto& conn : connections_) {
|
|
442
|
-
result.push_back(conn.get());
|
|
443
|
-
}
|
|
444
|
-
return result;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
std::vector<std::pair<std::string, uint16_t>> Torrent::get_pending_peers() const {
|
|
448
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
449
|
-
return pending_peers_;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
void Torrent::clear_pending_peers() {
|
|
453
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
454
|
-
pending_peers_.clear();
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
void Torrent::add_connection(std::shared_ptr<BtPeerConnection> connection) {
|
|
458
|
-
if (!connection) return;
|
|
459
|
-
{
|
|
460
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
461
|
-
|
|
462
|
-
LOG_DEBUG("Torrent", "Adding connection from " + connection->ip() + ":" +
|
|
463
|
-
std::to_string(connection->port()));
|
|
464
|
-
|
|
465
|
-
// Remove from pending if present
|
|
466
|
-
auto it = std::remove_if(pending_peers_.begin(), pending_peers_.end(),
|
|
467
|
-
[&](const auto& p) {
|
|
468
|
-
return p.first == connection->ip() && p.second == connection->port();
|
|
469
|
-
});
|
|
470
|
-
pending_peers_.erase(it, pending_peers_.end());
|
|
471
|
-
|
|
472
|
-
connection->set_message_callback(
|
|
473
|
-
[this](BtPeerConnection* peer, const BtMessage& msg) {
|
|
474
|
-
on_peer_message(peer, msg);
|
|
475
|
-
}
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
connection->set_state_callback(
|
|
479
|
-
[this](BtPeerConnection* peer, PeerConnectionState state) {
|
|
480
|
-
if (state == PeerConnectionState::Disconnected) {
|
|
481
|
-
on_peer_disconnected(peer);
|
|
482
|
-
} else if (state == PeerConnectionState::Connected) {
|
|
483
|
-
on_peer_connected(peer);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
// Add to picker for availability tracking
|
|
489
|
-
if (picker_ && connection->is_connected()) {
|
|
490
|
-
picker_->add_peer(connection.get(), connection->peer_pieces());
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
connections_.push_back(connection); // shared_ptr - just copy
|
|
494
|
-
|
|
495
|
-
// Notify peer we're connected and setup extension handshake
|
|
496
|
-
on_peer_connected(connection.get());
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Extended callback (rats-search compatibility)
|
|
500
|
-
if (on_peer_connected_ext_) {
|
|
501
|
-
Peer peer_address(connection->ip(), connection->port());
|
|
502
|
-
on_peer_connected_ext_(peer_address);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
void Torrent::remove_connection(BtPeerConnection* connection) {
|
|
507
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
508
|
-
|
|
509
|
-
auto it = std::find_if(connections_.begin(), connections_.end(),
|
|
510
|
-
[connection](const auto& conn) {
|
|
511
|
-
return conn.get() == connection;
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
if (it != connections_.end()) {
|
|
515
|
-
LOG_DEBUG("Torrent", "Removing connection from " + connection->ip() + ":" +
|
|
516
|
-
std::to_string(connection->port()));
|
|
517
|
-
|
|
518
|
-
// Cleanup peer from picker (cancel requests, remove from availability)
|
|
519
|
-
// Note: on_peer_disconnected may have already been called via state_callback,
|
|
520
|
-
// but these operations are idempotent
|
|
521
|
-
if (picker_) {
|
|
522
|
-
picker_->cancel_peer_requests(connection);
|
|
523
|
-
picker_->remove_peer(connection);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Clear metadata tracking for this peer
|
|
527
|
-
peer_metadata_size_.erase(connection);
|
|
528
|
-
peer_ut_metadata_id_.erase(connection);
|
|
529
|
-
|
|
530
|
-
connections_.erase(it);
|
|
531
|
-
LOG_DEBUG("Torrent", "Active connections: " + std::to_string(connections_.size()));
|
|
532
|
-
} else {
|
|
533
|
-
LOG_DEBUG("Torrent", "Connection not found for removal: " + connection->ip());
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
//=============================================================================
|
|
538
|
-
// Configuration
|
|
539
|
-
//=============================================================================
|
|
540
|
-
|
|
541
|
-
void Torrent::set_sequential(bool sequential) {
|
|
542
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
543
|
-
config_.sequential_download = sequential;
|
|
544
|
-
LOG_INFO("Torrent", "Sequential download " + std::string(sequential ? "enabled" : "disabled") +
|
|
545
|
-
" for '" + name_ + "'");
|
|
546
|
-
if (picker_) {
|
|
547
|
-
picker_->set_mode(sequential ? PickerMode::Sequential : PickerMode::RarestFirst);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
void Torrent::set_download_limit(uint64_t bytes_per_sec) {
|
|
552
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
553
|
-
config_.download_limit = bytes_per_sec;
|
|
554
|
-
LOG_INFO("Torrent", "Download limit set to " + std::to_string(bytes_per_sec) +
|
|
555
|
-
" bytes/sec for '" + name_ + "'");
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
void Torrent::set_upload_limit(uint64_t bytes_per_sec) {
|
|
559
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
560
|
-
config_.upload_limit = bytes_per_sec;
|
|
561
|
-
LOG_INFO("Torrent", "Upload limit set to " + std::to_string(bytes_per_sec) +
|
|
562
|
-
" bytes/sec for '" + name_ + "'");
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
//=============================================================================
|
|
566
|
-
// Metadata
|
|
567
|
-
//=============================================================================
|
|
568
|
-
|
|
569
|
-
bool Torrent::set_metadata(const std::vector<uint8_t>& metadata) {
|
|
570
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
571
|
-
|
|
572
|
-
LOG_INFO("Torrent", "Setting metadata (" + std::to_string(metadata.size()) + " bytes)");
|
|
573
|
-
|
|
574
|
-
auto new_info = TorrentInfo::from_info_dict(metadata, info_hash_);
|
|
575
|
-
if (!new_info) {
|
|
576
|
-
LOG_ERROR("Torrent", "Failed to parse metadata - info_hash mismatch or invalid data");
|
|
577
|
-
return false;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
info_ = std::make_unique<TorrentInfo>(*new_info);
|
|
581
|
-
name_ = info_->name();
|
|
582
|
-
LOG_INFO("Torrent", "Metadata parsed successfully: name='" + name_ + "'");
|
|
583
|
-
|
|
584
|
-
// Save any previously loaded resume data (from try_load_resume_data before metadata)
|
|
585
|
-
Bitfield saved_have_pieces = have_pieces_;
|
|
586
|
-
bool had_resume_data = (saved_have_pieces.size() > 0 && saved_have_pieces.count() > 0);
|
|
587
|
-
|
|
588
|
-
// Initialize pieces
|
|
589
|
-
have_pieces_ = Bitfield(info_->num_pieces());
|
|
590
|
-
|
|
591
|
-
uint32_t last_piece_len = info_->piece_size(info_->num_pieces() - 1);
|
|
592
|
-
picker_ = std::make_unique<PiecePicker>(
|
|
593
|
-
info_->num_pieces(),
|
|
594
|
-
info_->piece_length(),
|
|
595
|
-
last_piece_len
|
|
596
|
-
);
|
|
597
|
-
|
|
598
|
-
if (config_.sequential_download) {
|
|
599
|
-
picker_->set_mode(PickerMode::Sequential);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
stats_.total_size = info_->total_size();
|
|
603
|
-
stats_.pieces_total = info_->num_pieces();
|
|
604
|
-
|
|
605
|
-
// Restore resume data if we had it loaded before metadata arrived
|
|
606
|
-
if (had_resume_data && saved_have_pieces.size() == info_->num_pieces()) {
|
|
607
|
-
LOG_INFO("Torrent", "Restoring " + std::to_string(saved_have_pieces.count()) +
|
|
608
|
-
" pieces from resume data");
|
|
609
|
-
have_pieces_ = saved_have_pieces;
|
|
610
|
-
for (uint32_t i = 0; i < have_pieces_.size(); ++i) {
|
|
611
|
-
if (have_pieces_.get_bit(i)) {
|
|
612
|
-
picker_->mark_have(i);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
// Calculate bytes_done from pieces
|
|
616
|
-
stats_.pieces_done = picker_->num_have();
|
|
617
|
-
stats_.bytes_done = 0;
|
|
618
|
-
for (uint32_t i = 0; i < have_pieces_.size(); ++i) {
|
|
619
|
-
if (have_pieces_.get_bit(i)) {
|
|
620
|
-
stats_.bytes_done += info_->piece_size(i);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
stats_.progress = static_cast<float>(stats_.pieces_done) /
|
|
624
|
-
static_cast<float>(stats_.pieces_total);
|
|
625
|
-
LOG_INFO("Torrent", "Resume data restored: " + std::to_string(stats_.pieces_done) +
|
|
626
|
-
"/" + std::to_string(stats_.pieces_total) + " pieces (" +
|
|
627
|
-
std::to_string(stats_.bytes_done) + " bytes, " +
|
|
628
|
-
std::to_string(stats_.progress * 100.0f) + "%)");
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
LOG_DEBUG("Torrent", "Torrent stats: " + std::to_string(info_->num_pieces()) + " pieces, " +
|
|
632
|
-
std::to_string(info_->total_size()) + " bytes total");
|
|
633
|
-
|
|
634
|
-
// CRITICAL: Initialize existing peer connections with the new metadata
|
|
635
|
-
// This is necessary because peers connected during DownloadingMetadata state
|
|
636
|
-
// were not added to the picker (it didn't exist yet).
|
|
637
|
-
|
|
638
|
-
// In metadata-only mode (empty save_path), we skip sending INTERESTED and bitfield
|
|
639
|
-
// because we're not going to download anything - just needed the metadata
|
|
640
|
-
const bool metadata_only = config_.save_path.empty();
|
|
641
|
-
|
|
642
|
-
LOG_DEBUG("Torrent", "Initializing " + std::to_string(connections_.size()) +
|
|
643
|
-
" existing connections with metadata" +
|
|
644
|
-
(metadata_only ? " (metadata-only mode)" : ""));
|
|
645
|
-
|
|
646
|
-
for (auto& conn : connections_) {
|
|
647
|
-
LOG_DEBUG("Torrent", "Processing connection " + conn->ip() +
|
|
648
|
-
" is_connected=" + (conn->is_connected() ? "yes" : "no") +
|
|
649
|
-
" peer_has_all=" + (conn->peer_has_all() ? "yes" : "no") +
|
|
650
|
-
" peer_pieces_count=" + std::to_string(conn->peer_pieces().count()));
|
|
651
|
-
|
|
652
|
-
if (!conn->is_connected()) {
|
|
653
|
-
LOG_DEBUG("Torrent", "Skipping " + conn->ip() + " - not connected");
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Update peer's bitfield size to match the torrent
|
|
658
|
-
// This is needed because peer may have sent Bitfield/HaveAll before we had metadata
|
|
659
|
-
conn->set_torrent_info(info_hash_, info_->num_pieces());
|
|
660
|
-
|
|
661
|
-
// Skip download-related initialization in metadata-only mode
|
|
662
|
-
if (metadata_only) {
|
|
663
|
-
continue;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Get peer's pieces (now correctly sized)
|
|
667
|
-
const auto& peer_pieces = conn->peer_pieces();
|
|
668
|
-
|
|
669
|
-
// Add peer to picker for availability tracking
|
|
670
|
-
picker_->add_peer(conn.get(), peer_pieces);
|
|
671
|
-
|
|
672
|
-
// Check if this peer has pieces we need (either via bitfield or peer_has_all flag)
|
|
673
|
-
bool is_interesting = false;
|
|
674
|
-
if (conn->peer_has_all()) {
|
|
675
|
-
// Peer has everything, we have nothing - definitely interesting!
|
|
676
|
-
is_interesting = true;
|
|
677
|
-
LOG_DEBUG("Torrent", "Peer " + conn->ip() + " has all pieces (HaveAll)");
|
|
678
|
-
} else if (picker_->is_interesting(peer_pieces)) {
|
|
679
|
-
is_interesting = true;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
if (is_interesting) {
|
|
683
|
-
// Send INTERESTED to peer so they will (hopefully) unchoke us
|
|
684
|
-
if (!conn->am_interested()) {
|
|
685
|
-
LOG_DEBUG("Torrent", "Peer " + conn->ip() + " has pieces we need, sending interested");
|
|
686
|
-
conn->send_interested();
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Send our bitfield to the peer (we have no pieces yet, but protocol requires it)
|
|
691
|
-
conn->send_bitfield(have_pieces_);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
set_state(TorrentState::Downloading);
|
|
695
|
-
|
|
696
|
-
return true;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
//=============================================================================
|
|
700
|
-
// Tick
|
|
701
|
-
//=============================================================================
|
|
702
|
-
|
|
703
|
-
void Torrent::tick() {
|
|
704
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
705
|
-
|
|
706
|
-
if (!is_active()) {
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
auto now = std::chrono::steady_clock::now();
|
|
711
|
-
|
|
712
|
-
// Check metadata download timeout
|
|
713
|
-
if (state_ == TorrentState::DownloadingMetadata) {
|
|
714
|
-
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
|
715
|
-
now - metadata_download_started_).count();
|
|
716
|
-
|
|
717
|
-
if (elapsed >= METADATA_TIMEOUT_SECONDS) {
|
|
718
|
-
LOG_WARN("Torrent", "Metadata download timeout for " +
|
|
719
|
-
info_hash_to_hex(info_hash_).substr(0, 8) + "...");
|
|
720
|
-
|
|
721
|
-
// Notify callback of failure
|
|
722
|
-
if (on_metadata_received_) {
|
|
723
|
-
on_metadata_received_(this, false);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Clear metadata state
|
|
727
|
-
metadata_buffer_.clear();
|
|
728
|
-
metadata_pieces_received_.clear();
|
|
729
|
-
metadata_pieces_requested_.clear();
|
|
730
|
-
peer_metadata_size_.clear();
|
|
731
|
-
peer_ut_metadata_id_.clear();
|
|
732
|
-
|
|
733
|
-
// Set error state
|
|
734
|
-
error_message_ = "Metadata download timeout";
|
|
735
|
-
set_state(TorrentState::Error);
|
|
736
|
-
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Run choker periodically
|
|
742
|
-
if (choker_.should_rechoke()) {
|
|
743
|
-
run_choker();
|
|
744
|
-
last_choker_run_ = now;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Request pieces from peers
|
|
748
|
-
if (state_ == TorrentState::Downloading && picker_) {
|
|
749
|
-
// Log peer status periodically (every 5 seconds)
|
|
750
|
-
static auto last_peer_log = std::chrono::steady_clock::time_point{};
|
|
751
|
-
if ((now - last_peer_log) >= std::chrono::seconds(5)) {
|
|
752
|
-
LOG_DEBUG("Torrent", "Peer status for '" + name_ + "' (" +
|
|
753
|
-
std::to_string(connections_.size()) + " peers):");
|
|
754
|
-
for (const auto& conn : connections_) {
|
|
755
|
-
LOG_DEBUG("Torrent", " " + conn->ip() +
|
|
756
|
-
" connected=" + (conn->is_connected() ? "yes" : "no") +
|
|
757
|
-
" peer_choking=" + (conn->peer_choking() ? "yes" : "no") +
|
|
758
|
-
" am_interested=" + (conn->am_interested() ? "yes" : "no") +
|
|
759
|
-
" pieces=" + std::to_string(conn->peer_pieces().count()));
|
|
760
|
-
}
|
|
761
|
-
last_peer_log = now;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
request_pieces();
|
|
765
|
-
|
|
766
|
-
// Update statistics
|
|
767
|
-
auto stats_interval = std::chrono::seconds(1);
|
|
768
|
-
if ((now - last_stats_update_) >= stats_interval) {
|
|
769
|
-
update_stats();
|
|
770
|
-
last_stats_update_ = now;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// TODO: Connect to pending peers
|
|
774
|
-
// TODO: Announce to trackers
|
|
775
|
-
// TODO: DHT announces
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
//=============================================================================
|
|
779
|
-
// Internal Methods
|
|
780
|
-
//=============================================================================
|
|
781
|
-
|
|
782
|
-
void Torrent::on_peer_connected(BtPeerConnection* peer) {
|
|
783
|
-
LOG_DEBUG("Torrent", "Peer " + peer->ip() + " connected" +
|
|
784
|
-
(peer->peer_client_name().empty() ? "" : " [" + peer->peer_client_name() + "]") +
|
|
785
|
-
", has_metadata=" + (has_metadata_unlocked() ? "true" : "false"));
|
|
786
|
-
|
|
787
|
-
// Send extension handshake if peer supports it
|
|
788
|
-
if (peer->peer_extensions().extension_protocol) {
|
|
789
|
-
send_extension_handshake(peer);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Send bitfield if we have pieces
|
|
793
|
-
if (picker_ && picker_->num_have() > 0) {
|
|
794
|
-
peer->send_bitfield(picker_->get_have_bitfield());
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Check if peer already received extension handshake before we set up callbacks
|
|
798
|
-
// This happens because network data arrives before Torrent takes ownership
|
|
799
|
-
if (state_ == TorrentState::DownloadingMetadata &&
|
|
800
|
-
peer->extension_handshake_received() &&
|
|
801
|
-
peer->peer_metadata_size() > 0 &&
|
|
802
|
-
peer->peer_ut_metadata_id() != 0) {
|
|
803
|
-
|
|
804
|
-
LOG_DEBUG("Torrent", "Peer " + peer->ip() +
|
|
805
|
-
" already has extension handshake data: metadata_size=" +
|
|
806
|
-
std::to_string(peer->peer_metadata_size()) +
|
|
807
|
-
" ut_metadata_id=" + std::to_string(peer->peer_ut_metadata_id()));
|
|
808
|
-
|
|
809
|
-
// Store peer's metadata info and request metadata
|
|
810
|
-
peer_metadata_size_[peer] = peer->peer_metadata_size();
|
|
811
|
-
peer_ut_metadata_id_[peer] = peer->peer_ut_metadata_id();
|
|
812
|
-
request_metadata(peer);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
void Torrent::on_peer_disconnected(BtPeerConnection* peer) {
|
|
817
|
-
LOG_DEBUG("Torrent", "Peer " + peer->ip() + " disconnected");
|
|
818
|
-
|
|
819
|
-
// Extended callback (rats-search compatibility)
|
|
820
|
-
if (on_peer_disconnected_ext_) {
|
|
821
|
-
Peer p(peer->ip(), peer->port());
|
|
822
|
-
on_peer_disconnected_ext_(p);
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Cancel pending requests
|
|
826
|
-
if (picker_) {
|
|
827
|
-
picker_->cancel_peer_requests(peer);
|
|
828
|
-
LOG_DEBUG("Torrent", "Cancelled pending requests for peer " + peer->ip());
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Remove from availability
|
|
832
|
-
if (picker_) {
|
|
833
|
-
picker_->remove_peer(peer);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// Clear metadata tracking for this peer
|
|
837
|
-
peer_metadata_size_.erase(peer);
|
|
838
|
-
peer_ut_metadata_id_.erase(peer);
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
void Torrent::on_peer_message(BtPeerConnection* peer, const BtMessage& msg) {
|
|
842
|
-
// Log all message types for debugging
|
|
843
|
-
switch (msg.type) {
|
|
844
|
-
case BtMessageType::Bitfield:
|
|
845
|
-
if (msg.bitfield) {
|
|
846
|
-
LOG_DEBUG("Torrent", "on_peer_message: Bitfield from " + peer->ip() +
|
|
847
|
-
", bits_set=" + std::to_string(msg.bitfield->count()));
|
|
848
|
-
}
|
|
849
|
-
if (msg.bitfield && picker_) {
|
|
850
|
-
picker_->add_peer(peer, *msg.bitfield);
|
|
851
|
-
|
|
852
|
-
// Check if interesting
|
|
853
|
-
if (picker_->is_interesting(*msg.bitfield)) {
|
|
854
|
-
peer->send_interested();
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
break;
|
|
858
|
-
|
|
859
|
-
case BtMessageType::Have:
|
|
860
|
-
LOG_DEBUG("Torrent", "on_peer_message: Have piece=" + std::to_string(msg.have_piece) +
|
|
861
|
-
" from " + peer->ip());
|
|
862
|
-
if (picker_) {
|
|
863
|
-
picker_->peer_has_piece(peer, msg.have_piece);
|
|
864
|
-
// Check if we should become interested
|
|
865
|
-
if (!peer->am_interested() && !have_pieces_.get_bit(msg.have_piece)) {
|
|
866
|
-
peer->send_interested();
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
break;
|
|
870
|
-
|
|
871
|
-
case BtMessageType::HaveAll:
|
|
872
|
-
// Fast extension: peer has all pieces (seeder)
|
|
873
|
-
LOG_DEBUG("Torrent", "on_peer_message: HaveAll from " + peer->ip());
|
|
874
|
-
if (picker_) {
|
|
875
|
-
// Add peer to picker with all pieces
|
|
876
|
-
Bitfield all_pieces(info_ ? info_->num_pieces() : 0);
|
|
877
|
-
all_pieces.set_all();
|
|
878
|
-
picker_->add_peer(peer, all_pieces);
|
|
879
|
-
|
|
880
|
-
// We're definitely interested in a seeder
|
|
881
|
-
if (!peer->am_interested()) {
|
|
882
|
-
LOG_DEBUG("Torrent", "Peer " + peer->ip() + " has all pieces, sending interested");
|
|
883
|
-
peer->send_interested();
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
break;
|
|
887
|
-
|
|
888
|
-
case BtMessageType::HaveNone:
|
|
889
|
-
// Fast extension: peer has no pieces
|
|
890
|
-
LOG_DEBUG("Torrent", "on_peer_message: HaveNone from " + peer->ip());
|
|
891
|
-
break;
|
|
892
|
-
|
|
893
|
-
case BtMessageType::Piece:
|
|
894
|
-
if (msg.piece) {
|
|
895
|
-
LOG_DEBUG("Torrent", "on_peer_message: Piece idx=" + std::to_string(msg.piece->piece_index) +
|
|
896
|
-
" begin=" + std::to_string(msg.piece->begin) +
|
|
897
|
-
" len=" + std::to_string(msg.piece->data.size()) + " from " + peer->ip());
|
|
898
|
-
on_piece_received(msg.piece->piece_index, msg.piece->begin, msg.piece->data);
|
|
899
|
-
}
|
|
900
|
-
break;
|
|
901
|
-
|
|
902
|
-
case BtMessageType::Request:
|
|
903
|
-
// Peer is requesting a block from us
|
|
904
|
-
if (msg.request) {
|
|
905
|
-
LOG_DEBUG("Torrent", "on_peer_message: Request piece=" + std::to_string(msg.request->piece_index) +
|
|
906
|
-
" begin=" + std::to_string(msg.request->begin) +
|
|
907
|
-
" len=" + std::to_string(msg.request->length) + " from " + peer->ip());
|
|
908
|
-
}
|
|
909
|
-
if (msg.request && !peer->am_choking() && have_pieces_.get_bit(msg.request->piece_index)) {
|
|
910
|
-
// Track this request so we can handle Cancel
|
|
911
|
-
peer->add_incoming_request(*msg.request);
|
|
912
|
-
// Read from disk and send piece data to peer
|
|
913
|
-
read_piece_from_disk(msg.request->piece_index, peer,
|
|
914
|
-
msg.request->begin, msg.request->length);
|
|
915
|
-
}
|
|
916
|
-
break;
|
|
917
|
-
|
|
918
|
-
case BtMessageType::Cancel:
|
|
919
|
-
// Peer is cancelling a previous request (don't send the piece)
|
|
920
|
-
if (msg.request) {
|
|
921
|
-
LOG_DEBUG("Torrent", "on_peer_message: Cancel piece=" + std::to_string(msg.request->piece_index) +
|
|
922
|
-
" begin=" + std::to_string(msg.request->begin) +
|
|
923
|
-
" len=" + std::to_string(msg.request->length) + " from " + peer->ip());
|
|
924
|
-
peer->remove_incoming_request(*msg.request);
|
|
925
|
-
}
|
|
926
|
-
break;
|
|
927
|
-
|
|
928
|
-
case BtMessageType::RejectRequest:
|
|
929
|
-
// Peer rejected our request (Fast Extension) - need to request from another peer
|
|
930
|
-
if (msg.request) {
|
|
931
|
-
LOG_DEBUG("Torrent", "on_peer_message: RejectRequest piece=" + std::to_string(msg.request->piece_index) +
|
|
932
|
-
" begin=" + std::to_string(msg.request->begin) + " from " + peer->ip());
|
|
933
|
-
// Remove from pending and mark as available for re-request
|
|
934
|
-
if (picker_) {
|
|
935
|
-
BlockInfo block(msg.request->piece_index, msg.request->begin, msg.request->length);
|
|
936
|
-
picker_->abort_download(block, peer);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
break;
|
|
940
|
-
|
|
941
|
-
case BtMessageType::Unchoke:
|
|
942
|
-
// Peer unchoked us - can start requesting
|
|
943
|
-
LOG_DEBUG("Torrent", "on_peer_message: Unchoke from " + peer->ip());
|
|
944
|
-
// Immediately request blocks from this peer
|
|
945
|
-
if (picker_ && peer->am_interested()) {
|
|
946
|
-
request_blocks_from_peer(peer);
|
|
947
|
-
}
|
|
948
|
-
break;
|
|
949
|
-
|
|
950
|
-
case BtMessageType::Choke:
|
|
951
|
-
// Peer choked us - stop requesting
|
|
952
|
-
LOG_DEBUG("Torrent", "on_peer_message: Choke from " + peer->ip());
|
|
953
|
-
if (picker_) {
|
|
954
|
-
picker_->cancel_peer_requests(peer);
|
|
955
|
-
}
|
|
956
|
-
break;
|
|
957
|
-
|
|
958
|
-
case BtMessageType::Interested:
|
|
959
|
-
LOG_DEBUG("Torrent", "on_peer_message: Interested from " + peer->ip());
|
|
960
|
-
break;
|
|
961
|
-
|
|
962
|
-
case BtMessageType::NotInterested:
|
|
963
|
-
LOG_DEBUG("Torrent", "on_peer_message: NotInterested from " + peer->ip());
|
|
964
|
-
break;
|
|
965
|
-
|
|
966
|
-
case BtMessageType::Extended:
|
|
967
|
-
LOG_DEBUG("Torrent", "on_peer_message: Extended ext_id=" + std::to_string(msg.extension_id) +
|
|
968
|
-
" payload=" + std::to_string(msg.extension_payload.size()) + " bytes from " + peer->ip());
|
|
969
|
-
// Handle extension protocol messages
|
|
970
|
-
on_extension_message(peer, msg.extension_id, msg.extension_payload);
|
|
971
|
-
break;
|
|
972
|
-
|
|
973
|
-
default:
|
|
974
|
-
LOG_DEBUG("Torrent", "on_peer_message: Unknown type from " + peer->ip());
|
|
975
|
-
break;
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
void Torrent::on_piece_received(uint32_t piece, uint32_t begin,
|
|
980
|
-
const std::vector<uint8_t>& data) {
|
|
981
|
-
// Note: This is called from network thread, need to acquire mutex
|
|
982
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
983
|
-
|
|
984
|
-
if (!picker_ || !info_) {
|
|
985
|
-
LOG_WARN("Torrent", "Received piece data but picker/info not initialized");
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
LOG_DEBUG("Torrent", "Block received: piece=" + std::to_string(piece) +
|
|
990
|
-
" offset=" + std::to_string(begin) + " len=" + std::to_string(data.size()));
|
|
991
|
-
|
|
992
|
-
BlockInfo block(piece, begin, static_cast<uint32_t>(data.size()));
|
|
993
|
-
|
|
994
|
-
// Mark block as writing (will be marked finished after disk write completes)
|
|
995
|
-
picker_->mark_writing(block);
|
|
996
|
-
|
|
997
|
-
// Write block directly to disk (no in-memory buffering)
|
|
998
|
-
// This prevents memory bloat when downloading from many peers
|
|
999
|
-
write_block_to_disk(block, data);
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
void Torrent::on_piece_verified(uint32_t piece, bool valid) {
|
|
1003
|
-
if (!picker_) return;
|
|
1004
|
-
|
|
1005
|
-
if (valid) {
|
|
1006
|
-
LOG_INFO("Torrent", "Piece " + std::to_string(piece) + " verified successfully (" +
|
|
1007
|
-
std::to_string(picker_->num_have() + 1) + "/" +
|
|
1008
|
-
std::to_string(stats_.pieces_total) + ")");
|
|
1009
|
-
|
|
1010
|
-
picker_->mark_have(piece);
|
|
1011
|
-
have_pieces_.set_bit(piece);
|
|
1012
|
-
|
|
1013
|
-
// Blocks were written directly to disk - no buffer to clear
|
|
1014
|
-
|
|
1015
|
-
// Notify
|
|
1016
|
-
if (on_piece_complete_) {
|
|
1017
|
-
on_piece_complete_(this, piece);
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// Broadcast HAVE to peers
|
|
1021
|
-
for (auto& conn : connections_) {
|
|
1022
|
-
conn->send_have(piece);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// Check if complete
|
|
1026
|
-
if (picker_->is_complete()) {
|
|
1027
|
-
LOG_INFO("Torrent", "Download complete for '" + name_ + "'!");
|
|
1028
|
-
set_state(TorrentState::Seeding);
|
|
1029
|
-
choker_.set_seed_mode(true);
|
|
1030
|
-
|
|
1031
|
-
if (on_complete_) {
|
|
1032
|
-
on_complete_(this);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Extended callback (rats-search compatibility)
|
|
1036
|
-
if (on_torrent_complete_) {
|
|
1037
|
-
on_torrent_complete_(name_);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
++stats_.pieces_done;
|
|
1042
|
-
} else {
|
|
1043
|
-
LOG_WARN("Torrent", "Piece " + std::to_string(piece) + " hash verification FAILED, will re-download");
|
|
1044
|
-
// Hash check failed - need to re-download the piece
|
|
1045
|
-
// Reset the piece state in picker so it can be re-requested
|
|
1046
|
-
if (picker_) {
|
|
1047
|
-
// The piece was marked as 'have' prematurely - we need to remove it
|
|
1048
|
-
// from downloading state and allow it to be picked again
|
|
1049
|
-
// Note: mark_have was not called yet since verification failed
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
void Torrent::request_pieces() {
|
|
1055
|
-
if (!picker_) return;
|
|
1056
|
-
|
|
1057
|
-
for (auto& conn : connections_) {
|
|
1058
|
-
request_blocks_from_peer(conn.get());
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
void Torrent::request_blocks_from_peer(BtPeerConnection* peer) {
|
|
1063
|
-
if (!picker_ || !peer) return;
|
|
1064
|
-
|
|
1065
|
-
if (!peer->is_connected()) {
|
|
1066
|
-
LOG_DEBUG("Torrent", "request_blocks_from_peer: Peer " + peer->ip() + " is not connected, skipping request");
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
if (peer->peer_choking()) {
|
|
1070
|
-
LOG_DEBUG("Torrent", "request_blocks_from_peer: Peer " + peer->ip() + " is choking, skipping request");
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// Check disk I/O backpressure - don't request more blocks if disk can't keep up
|
|
1075
|
-
if (!DiskIO::instance().can_accept_write()) {
|
|
1076
|
-
LOG_DEBUG("Torrent", "request_blocks_from_peer: Disk I/O under backpressure, deferring requests");
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
int requested = 0;
|
|
1081
|
-
while (peer->can_request()) {
|
|
1082
|
-
auto blocks = picker_->pick_pieces(peer->peer_pieces(), 1, peer);
|
|
1083
|
-
if (blocks.empty()) {
|
|
1084
|
-
break;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
const auto& block = blocks[0].block;
|
|
1088
|
-
LOG_DEBUG("Torrent", "request_blocks_from_peer: Requesting piece=" + std::to_string(block.piece_index) +
|
|
1089
|
-
" offset=" + std::to_string(block.offset) +
|
|
1090
|
-
" len=" + std::to_string(block.length) + " from " + peer->ip());
|
|
1091
|
-
peer->send_request(block.piece_index, block.offset, block.length);
|
|
1092
|
-
++requested;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
if (requested > 0) {
|
|
1096
|
-
LOG_DEBUG("Torrent", "Requested " + std::to_string(requested) + " blocks from " + peer->ip());
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
void Torrent::run_choker() {
|
|
1101
|
-
std::vector<ChokePeerInfo> peer_infos;
|
|
1102
|
-
peer_infos.reserve(connections_.size());
|
|
1103
|
-
|
|
1104
|
-
for (auto& conn : connections_) {
|
|
1105
|
-
if (!conn->is_connected()) continue;
|
|
1106
|
-
|
|
1107
|
-
ChokePeerInfo info;
|
|
1108
|
-
info.connection = conn.get();
|
|
1109
|
-
info.download_rate = conn->stats().download_rate();
|
|
1110
|
-
info.upload_rate = conn->stats().upload_rate();
|
|
1111
|
-
info.am_choking = conn->am_choking();
|
|
1112
|
-
info.am_interested = conn->am_interested();
|
|
1113
|
-
info.peer_interested = conn->peer_interested();
|
|
1114
|
-
info.connected_at = conn->stats().connected_at;
|
|
1115
|
-
|
|
1116
|
-
peer_infos.push_back(info);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
auto result = choker_.run(peer_infos);
|
|
1120
|
-
|
|
1121
|
-
if (!result.to_choke.empty() || !result.to_unchoke.empty()) {
|
|
1122
|
-
LOG_DEBUG("Torrent", "Choker: choking " + std::to_string(result.to_choke.size()) +
|
|
1123
|
-
" peers, unchoking " + std::to_string(result.to_unchoke.size()) + " peers");
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
for (auto* peer : result.to_choke) {
|
|
1127
|
-
LOG_DEBUG("Torrent", "Choking peer " + peer->ip());
|
|
1128
|
-
peer->send_choke();
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
for (auto* peer : result.to_unchoke) {
|
|
1132
|
-
LOG_DEBUG("Torrent", "Unchoking peer " + peer->ip());
|
|
1133
|
-
peer->send_unchoke();
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
void Torrent::update_stats() {
|
|
1138
|
-
if (picker_) {
|
|
1139
|
-
stats_.pieces_done = picker_->num_have();
|
|
1140
|
-
stats_.progress = static_cast<float>(stats_.pieces_done) /
|
|
1141
|
-
static_cast<float>(stats_.pieces_total);
|
|
1142
|
-
|
|
1143
|
-
// Calculate bytes done
|
|
1144
|
-
if (info_) {
|
|
1145
|
-
uint64_t bytes = 0;
|
|
1146
|
-
for (uint32_t i = 0; i < stats_.pieces_done; ++i) {
|
|
1147
|
-
bytes += info_->piece_size(i);
|
|
1148
|
-
}
|
|
1149
|
-
stats_.bytes_done = bytes;
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
stats_.peers_connected = static_cast<uint32_t>(connections_.size());
|
|
1154
|
-
stats_.peers_total = static_cast<uint32_t>(
|
|
1155
|
-
connections_.size() + pending_peers_.size());
|
|
1156
|
-
|
|
1157
|
-
// Count seeders/leechers
|
|
1158
|
-
stats_.seeders = 0;
|
|
1159
|
-
stats_.leechers = 0;
|
|
1160
|
-
for (const auto& conn : connections_) {
|
|
1161
|
-
if (conn->peer_pieces().all_set()) {
|
|
1162
|
-
++stats_.seeders;
|
|
1163
|
-
} else {
|
|
1164
|
-
++stats_.leechers;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Calculate rates
|
|
1169
|
-
uint64_t total_dl = 0, total_ul = 0;
|
|
1170
|
-
for (const auto& conn : connections_) {
|
|
1171
|
-
total_dl += static_cast<uint64_t>(conn->stats().download_rate());
|
|
1172
|
-
total_ul += static_cast<uint64_t>(conn->stats().upload_rate());
|
|
1173
|
-
}
|
|
1174
|
-
stats_.download_rate = total_dl;
|
|
1175
|
-
stats_.upload_rate = total_ul;
|
|
1176
|
-
|
|
1177
|
-
// Calculate ETA
|
|
1178
|
-
if (stats_.download_rate > 0 && stats_.bytes_done < stats_.total_size) {
|
|
1179
|
-
uint64_t remaining = stats_.total_size - stats_.bytes_done;
|
|
1180
|
-
stats_.eta = std::chrono::seconds(remaining / stats_.download_rate);
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// Log progress (only when downloading)
|
|
1184
|
-
if (state_ == TorrentState::Downloading && stats_.total_size > 0) {
|
|
1185
|
-
LOG_INFO("Torrent", "[" + name_ + "] " +
|
|
1186
|
-
std::to_string(static_cast<int>(stats_.progress * 100)) + "% | " +
|
|
1187
|
-
format_size(stats_.bytes_done) + " / " + format_size(stats_.total_size) + " | " +
|
|
1188
|
-
"DL: " + format_speed(stats_.download_rate) + " | " +
|
|
1189
|
-
"UL: " + format_speed(stats_.upload_rate) + " | " +
|
|
1190
|
-
"Peers: " + std::to_string(stats_.peers_connected) +
|
|
1191
|
-
" (" + std::to_string(stats_.seeders) + "S/" +
|
|
1192
|
-
std::to_string(stats_.leechers) + "L) | " +
|
|
1193
|
-
"Pieces: " + std::to_string(stats_.pieces_done) + "/" +
|
|
1194
|
-
std::to_string(stats_.pieces_total) + " | " +
|
|
1195
|
-
"ETA: " + format_eta(stats_.eta));
|
|
1196
|
-
|
|
1197
|
-
// Log individual peer stats
|
|
1198
|
-
for (const auto& conn : connections_) {
|
|
1199
|
-
if (!conn->is_connected()) continue;
|
|
1200
|
-
|
|
1201
|
-
const auto& ps = conn->stats();
|
|
1202
|
-
uint32_t peer_pieces = static_cast<uint32_t>(conn->peer_pieces().count());
|
|
1203
|
-
|
|
1204
|
-
// Build state flags string
|
|
1205
|
-
std::string flags;
|
|
1206
|
-
flags += conn->am_interested() ? "I" : "-"; // we are interested
|
|
1207
|
-
flags += conn->peer_interested() ? "i" : "-"; // peer interested in us
|
|
1208
|
-
flags += conn->am_choking() ? "C" : "-"; // we are choking
|
|
1209
|
-
flags += conn->peer_choking() ? "c" : "-"; // peer choking us
|
|
1210
|
-
|
|
1211
|
-
bool is_seeder = conn->peer_pieces().all_set() || conn->peer_has_all();
|
|
1212
|
-
|
|
1213
|
-
LOG_INFO("Torrent", " Peer " + conn->ip() + ":" + std::to_string(conn->port()) +
|
|
1214
|
-
" | " + flags +
|
|
1215
|
-
" | DL: " + format_speed(static_cast<uint64_t>(ps.download_rate())) +
|
|
1216
|
-
" | UL: " + format_speed(static_cast<uint64_t>(ps.upload_rate())) +
|
|
1217
|
-
" | Req: " + std::to_string(conn->pending_requests()) +
|
|
1218
|
-
" | Got: " + format_size(ps.bytes_downloaded) +
|
|
1219
|
-
" | Pieces: " + std::to_string(peer_pieces) + "/" + std::to_string(stats_.pieces_total) +
|
|
1220
|
-
(is_seeder ? " [Seeder]" : ""));
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// Extended progress callback (rats-search compatibility)
|
|
1225
|
-
if (on_progress_ && stats_.total_size > 0) {
|
|
1226
|
-
double percentage = stats_.progress * 100.0;
|
|
1227
|
-
on_progress_(stats_.bytes_done, stats_.total_size, percentage);
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
//=============================================================================
|
|
1232
|
-
// Disk I/O Helpers
|
|
1233
|
-
//=============================================================================
|
|
1234
|
-
|
|
1235
|
-
std::vector<FileMappingInfo> Torrent::get_file_mappings() const {
|
|
1236
|
-
std::vector<FileMappingInfo> mappings;
|
|
1237
|
-
|
|
1238
|
-
if (!info_) return mappings;
|
|
1239
|
-
|
|
1240
|
-
const auto& files = info_->files().files();
|
|
1241
|
-
mappings.reserve(files.size());
|
|
1242
|
-
|
|
1243
|
-
for (const auto& file : files) {
|
|
1244
|
-
FileMappingInfo mapping;
|
|
1245
|
-
mapping.path = file.path;
|
|
1246
|
-
mapping.length = static_cast<uint64_t>(file.size);
|
|
1247
|
-
mapping.torrent_offset = static_cast<uint64_t>(file.offset);
|
|
1248
|
-
mappings.push_back(mapping);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
return mappings;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
void Torrent::write_block_to_disk(const BlockInfo& block, const std::vector<uint8_t>& data) {
|
|
1255
|
-
// Note: Called with mutex_ already held from on_piece_received
|
|
1256
|
-
if (!info_) return;
|
|
1257
|
-
|
|
1258
|
-
LOG_DEBUG("Torrent", "Writing block to disk: piece=" + std::to_string(block.piece_index) +
|
|
1259
|
-
" offset=" + std::to_string(block.offset) + " len=" + std::to_string(data.size()));
|
|
1260
|
-
|
|
1261
|
-
auto weak_self = weak_from_this();
|
|
1262
|
-
|
|
1263
|
-
DiskIO::instance().async_write_block(
|
|
1264
|
-
config_.save_path,
|
|
1265
|
-
get_file_mappings(),
|
|
1266
|
-
block.piece_index,
|
|
1267
|
-
info_->piece_length(),
|
|
1268
|
-
block.offset,
|
|
1269
|
-
data,
|
|
1270
|
-
[weak_self, block](bool success) {
|
|
1271
|
-
auto self = weak_self.lock();
|
|
1272
|
-
if (!self) return;
|
|
1273
|
-
|
|
1274
|
-
std::lock_guard<std::mutex> lock(self->mutex_);
|
|
1275
|
-
|
|
1276
|
-
if (success) {
|
|
1277
|
-
LOG_DEBUG("Torrent", "Block written to disk: piece=" + std::to_string(block.piece_index) +
|
|
1278
|
-
" offset=" + std::to_string(block.offset));
|
|
1279
|
-
|
|
1280
|
-
// Write successful - mark block as finished
|
|
1281
|
-
if (self->picker_) {
|
|
1282
|
-
bool piece_complete = self->picker_->mark_finished(block);
|
|
1283
|
-
|
|
1284
|
-
if (piece_complete) {
|
|
1285
|
-
LOG_INFO("Torrent", "Piece " + std::to_string(block.piece_index) +
|
|
1286
|
-
" complete (all blocks written), verifying hash");
|
|
1287
|
-
self->verify_piece_hash(block.piece_index);
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
} else {
|
|
1291
|
-
// Handle write error - abort the block so it can be re-requested
|
|
1292
|
-
LOG_ERROR("Torrent", "Failed to write block to disk: piece=" +
|
|
1293
|
-
std::to_string(block.piece_index) + " offset=" + std::to_string(block.offset));
|
|
1294
|
-
if (self->picker_) {
|
|
1295
|
-
self->picker_->abort_download(block, nullptr);
|
|
1296
|
-
}
|
|
1297
|
-
if (self->on_error_) {
|
|
1298
|
-
self->on_error_(self.get(), "Failed to write block to disk");
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
);
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
void Torrent::read_piece_from_disk(uint32_t piece, BtPeerConnection* peer,
|
|
1306
|
-
uint32_t begin, uint32_t length) {
|
|
1307
|
-
if (!info_) return;
|
|
1308
|
-
|
|
1309
|
-
LOG_DEBUG("Torrent", "Reading piece " + std::to_string(piece) + " from disk for peer " +
|
|
1310
|
-
peer->ip() + " (offset=" + std::to_string(begin) + ", len=" + std::to_string(length) + ")");
|
|
1311
|
-
|
|
1312
|
-
auto mappings = get_file_mappings();
|
|
1313
|
-
auto weak_self = weak_from_this();
|
|
1314
|
-
uint32_t actual_length = info_->piece_size(piece);
|
|
1315
|
-
|
|
1316
|
-
DiskIO::instance().async_read_piece(
|
|
1317
|
-
config_.save_path,
|
|
1318
|
-
mappings,
|
|
1319
|
-
piece,
|
|
1320
|
-
info_->piece_length(),
|
|
1321
|
-
actual_length,
|
|
1322
|
-
[weak_self, peer, piece, begin, length](bool success, const std::vector<uint8_t>& data) {
|
|
1323
|
-
auto self = weak_self.lock();
|
|
1324
|
-
if (!self) return;
|
|
1325
|
-
|
|
1326
|
-
if (success && data.size() >= begin + length) {
|
|
1327
|
-
// Send the requested block to peer
|
|
1328
|
-
peer->send_piece(piece, begin, data.data() + begin, length);
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
);
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
void Torrent::verify_piece_hash(uint32_t piece) {
|
|
1335
|
-
// Note: This method may be called with or without mutex held.
|
|
1336
|
-
// The async callback will acquire its own lock.
|
|
1337
|
-
if (!info_) return;
|
|
1338
|
-
|
|
1339
|
-
LOG_DEBUG("Torrent", "Verifying hash for piece " + std::to_string(piece));
|
|
1340
|
-
|
|
1341
|
-
auto mappings = get_file_mappings();
|
|
1342
|
-
auto weak_self = weak_from_this();
|
|
1343
|
-
uint32_t actual_length = info_->piece_size(piece);
|
|
1344
|
-
|
|
1345
|
-
DiskIO::instance().async_hash_piece(
|
|
1346
|
-
config_.save_path,
|
|
1347
|
-
mappings,
|
|
1348
|
-
piece,
|
|
1349
|
-
info_->piece_length(),
|
|
1350
|
-
actual_length,
|
|
1351
|
-
[weak_self, piece](bool success, const std::string& calculated_hash) {
|
|
1352
|
-
auto self = weak_self.lock();
|
|
1353
|
-
if (!self) return;
|
|
1354
|
-
|
|
1355
|
-
// Acquire lock for on_piece_verified which modifies shared state
|
|
1356
|
-
std::lock_guard<std::mutex> lock(self->mutex_);
|
|
1357
|
-
|
|
1358
|
-
if (!success) {
|
|
1359
|
-
self->on_piece_verified(piece, false);
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
// Compare with expected hash
|
|
1364
|
-
auto expected_hash = self->info_->piece_hash(piece);
|
|
1365
|
-
std::string expected_hex;
|
|
1366
|
-
for (uint8_t b : expected_hash) {
|
|
1367
|
-
char hex[3];
|
|
1368
|
-
snprintf(hex, sizeof(hex), "%02x", b);
|
|
1369
|
-
expected_hex += hex;
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
bool valid = (calculated_hash == expected_hex);
|
|
1373
|
-
self->on_piece_verified(piece, valid);
|
|
1374
|
-
}
|
|
1375
|
-
);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
//=============================================================================
|
|
1379
|
-
// Extension Protocol Methods
|
|
1380
|
-
//=============================================================================
|
|
1381
|
-
|
|
1382
|
-
void Torrent::send_extension_handshake(BtPeerConnection* peer) {
|
|
1383
|
-
// Create extension handshake
|
|
1384
|
-
BencodeValue handshake = BencodeValue::create_dict();
|
|
1385
|
-
|
|
1386
|
-
// Build 'm' dictionary with extension name -> message ID
|
|
1387
|
-
BencodeValue m = BencodeValue::create_dict();
|
|
1388
|
-
m["ut_metadata"] = BencodeValue(static_cast<int64_t>(BT_EXT_UT_METADATA_ID));
|
|
1389
|
-
m["ut_pex"] = BencodeValue(static_cast<int64_t>(BT_EXT_UT_PEX_ID));
|
|
1390
|
-
handshake["m"] = m;
|
|
1391
|
-
|
|
1392
|
-
// Add metadata_size if we have metadata
|
|
1393
|
-
if (info_ && info_->has_metadata()) {
|
|
1394
|
-
// We'd need the raw info dict size here
|
|
1395
|
-
// For now, skip this - we're mainly interested in receiving metadata
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// Add client ID
|
|
1399
|
-
handshake["v"] = BencodeValue("librats/1.0");
|
|
1400
|
-
|
|
1401
|
-
// Encode and send as extended message ID 0 (handshake)
|
|
1402
|
-
auto payload = handshake.encode();
|
|
1403
|
-
peer->send_extended(0, payload);
|
|
1404
|
-
|
|
1405
|
-
LOG_DEBUG("Torrent", "Sent extension handshake to " + peer->ip());
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
void Torrent::on_extension_message(BtPeerConnection* peer, uint8_t ext_id,
|
|
1409
|
-
const std::vector<uint8_t>& payload) {
|
|
1410
|
-
LOG_DEBUG("Torrent", "on_extension_message: ext_id=" + std::to_string(ext_id) +
|
|
1411
|
-
" payload=" + std::to_string(payload.size()) + " bytes from " + peer->ip() +
|
|
1412
|
-
" (state=" + std::string(torrent_state_to_string(state_)) + ")");
|
|
1413
|
-
|
|
1414
|
-
if (ext_id == 0) {
|
|
1415
|
-
// Extension handshake
|
|
1416
|
-
LOG_DEBUG("Torrent", "Processing extension handshake from " + peer->ip());
|
|
1417
|
-
on_extension_handshake(peer, payload);
|
|
1418
|
-
} else if (ext_id == BT_EXT_UT_METADATA_ID) {
|
|
1419
|
-
// ut_metadata message (our local ID = 1)
|
|
1420
|
-
if (state_ == TorrentState::DownloadingMetadata) {
|
|
1421
|
-
LOG_DEBUG("Torrent", "Processing ut_metadata message from " + peer->ip());
|
|
1422
|
-
on_metadata_message(peer, payload);
|
|
1423
|
-
} else {
|
|
1424
|
-
LOG_DEBUG("Torrent", "Ignoring ut_metadata message (already have metadata)");
|
|
1425
|
-
}
|
|
1426
|
-
} else if (ext_id == BT_EXT_UT_PEX_ID) {
|
|
1427
|
-
// ut_pex message (our local ID = 2)
|
|
1428
|
-
LOG_DEBUG("Torrent", "Received ut_pex message from " + peer->ip() + " (not implemented)");
|
|
1429
|
-
// TODO: Handle PEX for peer discovery
|
|
1430
|
-
} else {
|
|
1431
|
-
LOG_DEBUG("Torrent", "Unknown extension message ext_id=" + std::to_string(ext_id) +
|
|
1432
|
-
" from " + peer->ip());
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
void Torrent::on_extension_handshake(BtPeerConnection* peer,
|
|
1437
|
-
const std::vector<uint8_t>& payload) {
|
|
1438
|
-
BencodeValue decoded;
|
|
1439
|
-
try {
|
|
1440
|
-
decoded = BencodeDecoder::decode(payload);
|
|
1441
|
-
} catch (...) {
|
|
1442
|
-
LOG_WARN("Torrent", "Failed to decode extension handshake from " + peer->ip());
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
if (!decoded.is_dict()) {
|
|
1447
|
-
return;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
const auto& dict = decoded.as_dict();
|
|
1451
|
-
|
|
1452
|
-
// Extract metadata_size if present (for magnet links)
|
|
1453
|
-
auto metadata_size_it = dict.find("metadata_size");
|
|
1454
|
-
if (metadata_size_it != dict.end() && metadata_size_it->second.is_integer()) {
|
|
1455
|
-
size_t metadata_size = static_cast<size_t>(metadata_size_it->second.as_integer());
|
|
1456
|
-
LOG_INFO("Torrent", "Peer " + peer->ip() + " has metadata, size=" +
|
|
1457
|
-
std::to_string(metadata_size));
|
|
1458
|
-
|
|
1459
|
-
// If we're downloading metadata, store this info and start requesting
|
|
1460
|
-
if (state_ == TorrentState::DownloadingMetadata && metadata_size > 0) {
|
|
1461
|
-
// Store metadata size for this peer
|
|
1462
|
-
peer_metadata_size_[peer] = metadata_size;
|
|
1463
|
-
|
|
1464
|
-
// Parse the peer's ut_metadata ID from 'm' dict
|
|
1465
|
-
auto m_it = dict.find("m");
|
|
1466
|
-
if (m_it != dict.end() && m_it->second.is_dict()) {
|
|
1467
|
-
const auto& m = m_it->second.as_dict();
|
|
1468
|
-
auto ut_meta_it = m.find("ut_metadata");
|
|
1469
|
-
if (ut_meta_it != m.end() && ut_meta_it->second.is_integer()) {
|
|
1470
|
-
uint8_t peer_ut_metadata_id = static_cast<uint8_t>(ut_meta_it->second.as_integer());
|
|
1471
|
-
peer_ut_metadata_id_[peer] = peer_ut_metadata_id;
|
|
1472
|
-
|
|
1473
|
-
LOG_DEBUG("Torrent", "Peer " + peer->ip() + " ut_metadata ID = " +
|
|
1474
|
-
std::to_string(peer_ut_metadata_id));
|
|
1475
|
-
|
|
1476
|
-
// Start requesting metadata pieces
|
|
1477
|
-
request_metadata(peer);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
LOG_DEBUG("Torrent", "Received extension handshake from " + peer->ip());
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
void Torrent::request_metadata(BtPeerConnection* peer) {
|
|
1487
|
-
if (has_metadata_unlocked()) {
|
|
1488
|
-
LOG_DEBUG("Torrent", "request_metadata: already have metadata, skipping");
|
|
1489
|
-
return; // Already have metadata
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
auto size_it = peer_metadata_size_.find(peer);
|
|
1493
|
-
auto id_it = peer_ut_metadata_id_.find(peer);
|
|
1494
|
-
|
|
1495
|
-
if (size_it == peer_metadata_size_.end() || id_it == peer_ut_metadata_id_.end()) {
|
|
1496
|
-
LOG_DEBUG("Torrent", "request_metadata: no metadata info for peer " + peer->ip() +
|
|
1497
|
-
" (size_found=" + (size_it != peer_metadata_size_.end() ? "yes" : "no") +
|
|
1498
|
-
", id_found=" + (id_it != peer_ut_metadata_id_.end() ? "yes" : "no") + ")");
|
|
1499
|
-
return; // Don't have peer's metadata info
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
size_t metadata_size = size_it->second;
|
|
1503
|
-
uint8_t peer_ut_id = id_it->second;
|
|
1504
|
-
|
|
1505
|
-
// Calculate number of pieces (16KB each)
|
|
1506
|
-
uint32_t num_pieces = static_cast<uint32_t>((metadata_size + BT_METADATA_PIECE_SIZE - 1) / BT_METADATA_PIECE_SIZE);
|
|
1507
|
-
|
|
1508
|
-
// Initialize metadata buffer if needed
|
|
1509
|
-
if (metadata_buffer_.empty()) {
|
|
1510
|
-
metadata_buffer_.resize(metadata_size);
|
|
1511
|
-
metadata_pieces_received_.resize(num_pieces, false);
|
|
1512
|
-
metadata_pieces_requested_.resize(num_pieces, false);
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
// Request pieces we don't have yet (pipeline up to 4 requests)
|
|
1516
|
-
int requests_sent = 0;
|
|
1517
|
-
const int max_pending_requests = 4;
|
|
1518
|
-
|
|
1519
|
-
for (uint32_t piece = 0; piece < num_pieces; ++piece) {
|
|
1520
|
-
if (!metadata_pieces_received_[piece] && !metadata_pieces_requested_[piece]) {
|
|
1521
|
-
// Create request message
|
|
1522
|
-
BencodeValue req = BencodeValue::create_dict();
|
|
1523
|
-
req["msg_type"] = BencodeValue(static_cast<int64_t>(0)); // Request
|
|
1524
|
-
req["piece"] = BencodeValue(static_cast<int64_t>(piece));
|
|
1525
|
-
|
|
1526
|
-
auto req_payload = req.encode();
|
|
1527
|
-
peer->send_extended(peer_ut_id, req_payload);
|
|
1528
|
-
metadata_pieces_requested_[piece] = true;
|
|
1529
|
-
|
|
1530
|
-
LOG_DEBUG("Torrent", "Requesting metadata piece " + std::to_string(piece) +
|
|
1531
|
-
" from " + peer->ip());
|
|
1532
|
-
if (++requests_sent >= max_pending_requests) break;
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
void Torrent::on_metadata_message(BtPeerConnection* peer,
|
|
1538
|
-
const std::vector<uint8_t>& payload) {
|
|
1539
|
-
LOG_DEBUG("Torrent", "on_metadata_message: " + std::to_string(payload.size()) +
|
|
1540
|
-
" bytes from " + peer->ip());
|
|
1541
|
-
|
|
1542
|
-
// Parse the bencoded message
|
|
1543
|
-
BencodeValue decoded;
|
|
1544
|
-
try {
|
|
1545
|
-
decoded = BencodeDecoder::decode(payload);
|
|
1546
|
-
} catch (...) {
|
|
1547
|
-
LOG_WARN("Torrent", "Failed to decode metadata message from " + peer->ip());
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
if (!decoded.is_dict()) {
|
|
1552
|
-
LOG_WARN("Torrent", "Metadata message is not a dict from " + peer->ip());
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
const auto& dict = decoded.as_dict();
|
|
1557
|
-
|
|
1558
|
-
auto msg_type_it = dict.find("msg_type");
|
|
1559
|
-
if (msg_type_it == dict.end() || !msg_type_it->second.is_integer()) {
|
|
1560
|
-
LOG_WARN("Torrent", "Metadata message missing msg_type from " + peer->ip());
|
|
1561
|
-
return;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
int msg_type = static_cast<int>(msg_type_it->second.as_integer());
|
|
1565
|
-
LOG_DEBUG("Torrent", "on_metadata_message: msg_type=" + std::to_string(msg_type) +
|
|
1566
|
-
" (0=request, 1=data, 2=reject) from " + peer->ip());
|
|
1567
|
-
|
|
1568
|
-
if (msg_type == 1) { // Data
|
|
1569
|
-
auto piece_it = dict.find("piece");
|
|
1570
|
-
if (piece_it == dict.end() || !piece_it->second.is_integer()) {
|
|
1571
|
-
return;
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
uint32_t piece = static_cast<uint32_t>(piece_it->second.as_integer());
|
|
1575
|
-
|
|
1576
|
-
// Find where the data starts (after the bencoded dict)
|
|
1577
|
-
size_t dict_end = find_bencode_end(payload);
|
|
1578
|
-
if (dict_end >= payload.size()) {
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// Copy data to buffer
|
|
1583
|
-
size_t offset = static_cast<size_t>(piece) * BT_METADATA_PIECE_SIZE;
|
|
1584
|
-
size_t data_size = payload.size() - dict_end;
|
|
1585
|
-
|
|
1586
|
-
if (offset + data_size <= metadata_buffer_.size()) {
|
|
1587
|
-
std::memcpy(metadata_buffer_.data() + offset, payload.data() + dict_end, data_size);
|
|
1588
|
-
|
|
1589
|
-
if (piece < metadata_pieces_received_.size()) {
|
|
1590
|
-
metadata_pieces_received_[piece] = true;
|
|
1591
|
-
metadata_pieces_requested_[piece] = false;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
LOG_DEBUG("Torrent", "Received metadata piece " + std::to_string(piece) +
|
|
1595
|
-
" (" + std::to_string(data_size) + " bytes)");
|
|
1596
|
-
|
|
1597
|
-
// Check if we have all pieces
|
|
1598
|
-
bool complete = true;
|
|
1599
|
-
for (bool received : metadata_pieces_received_) {
|
|
1600
|
-
if (!received) {
|
|
1601
|
-
complete = false;
|
|
1602
|
-
break;
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
if (complete) {
|
|
1607
|
-
on_metadata_complete();
|
|
1608
|
-
} else {
|
|
1609
|
-
// Request next piece
|
|
1610
|
-
request_metadata(peer);
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
} else if (msg_type == 2) { // Reject
|
|
1614
|
-
LOG_DEBUG("Torrent", "Peer " + peer->ip() + " rejected metadata request");
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
size_t Torrent::find_bencode_end(const std::vector<uint8_t>& data) {
|
|
1619
|
-
// Find the end of a bencoded dictionary
|
|
1620
|
-
int depth = 0;
|
|
1621
|
-
for (size_t i = 0; i < data.size(); ++i) {
|
|
1622
|
-
if (data[i] == 'd' || data[i] == 'l') {
|
|
1623
|
-
++depth;
|
|
1624
|
-
} else if (data[i] == 'e') {
|
|
1625
|
-
--depth;
|
|
1626
|
-
if (depth == 0) {
|
|
1627
|
-
return i + 1;
|
|
1628
|
-
}
|
|
1629
|
-
} else if (std::isdigit(data[i])) {
|
|
1630
|
-
// Skip string
|
|
1631
|
-
size_t len = 0;
|
|
1632
|
-
while (i < data.size() && std::isdigit(data[i])) {
|
|
1633
|
-
len = len * 10 + (data[i] - '0');
|
|
1634
|
-
++i;
|
|
1635
|
-
}
|
|
1636
|
-
if (i < data.size() && data[i] == ':') {
|
|
1637
|
-
i += len; // Skip the string content
|
|
1638
|
-
}
|
|
1639
|
-
} else if (data[i] == 'i') {
|
|
1640
|
-
// Skip integer
|
|
1641
|
-
while (i < data.size() && data[i] != 'e') {
|
|
1642
|
-
++i;
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
return data.size();
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
void Torrent::on_metadata_complete() {
|
|
1650
|
-
LOG_INFO("Torrent", "Metadata download complete!");
|
|
1651
|
-
|
|
1652
|
-
// Validate and set metadata
|
|
1653
|
-
if (set_metadata(metadata_buffer_)) {
|
|
1654
|
-
LOG_INFO("Torrent", "Metadata validated, starting download of " + name_);
|
|
1655
|
-
|
|
1656
|
-
// Clear metadata tracking data
|
|
1657
|
-
metadata_buffer_.clear();
|
|
1658
|
-
metadata_pieces_received_.clear();
|
|
1659
|
-
metadata_pieces_requested_.clear();
|
|
1660
|
-
peer_metadata_size_.clear();
|
|
1661
|
-
peer_ut_metadata_id_.clear();
|
|
1662
|
-
|
|
1663
|
-
// State transition happens in set_metadata()
|
|
1664
|
-
|
|
1665
|
-
// Notify callback that metadata was received successfully
|
|
1666
|
-
if (on_metadata_received_) {
|
|
1667
|
-
on_metadata_received_(this, true);
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
// Extended callback with TorrentInfo (rats-search compatibility)
|
|
1671
|
-
if (on_metadata_complete_ && info_) {
|
|
1672
|
-
on_metadata_complete_(*info_);
|
|
1673
|
-
}
|
|
1674
|
-
} else {
|
|
1675
|
-
LOG_ERROR("Torrent", "Metadata validation failed");
|
|
1676
|
-
|
|
1677
|
-
// Notify callback of failure
|
|
1678
|
-
if (on_metadata_received_) {
|
|
1679
|
-
on_metadata_received_(this, false);
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
// Could retry from other peers
|
|
1683
|
-
metadata_buffer_.clear();
|
|
1684
|
-
metadata_pieces_received_.clear();
|
|
1685
|
-
metadata_pieces_requested_.clear();
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
//=============================================================================
|
|
1690
|
-
// Resume Data Methods
|
|
1691
|
-
//=============================================================================
|
|
1692
|
-
|
|
1693
|
-
TorrentResumeData Torrent::generate_resume_data() const {
|
|
1694
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
1695
|
-
|
|
1696
|
-
TorrentResumeData data;
|
|
1697
|
-
|
|
1698
|
-
// Identification
|
|
1699
|
-
data.info_hash = info_hash_;
|
|
1700
|
-
data.name = name_;
|
|
1701
|
-
data.save_path = config_.save_path;
|
|
1702
|
-
|
|
1703
|
-
// Have pieces
|
|
1704
|
-
if (picker_) {
|
|
1705
|
-
data.have_pieces = picker_->get_have_bitfield();
|
|
1706
|
-
} else {
|
|
1707
|
-
data.have_pieces = have_pieces_;
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
// Unfinished pieces (partial downloads)
|
|
1711
|
-
if (picker_) {
|
|
1712
|
-
auto downloading = picker_->downloading_pieces();
|
|
1713
|
-
for (const auto& dp : downloading) {
|
|
1714
|
-
if (dp.blocks_finished > 0 && dp.blocks_finished < dp.blocks_total) {
|
|
1715
|
-
// This piece has some but not all blocks - save state
|
|
1716
|
-
Bitfield blocks(dp.blocks_total);
|
|
1717
|
-
|
|
1718
|
-
// Get block states from piece picker
|
|
1719
|
-
uint32_t piece_len = info_ ? info_->piece_length() : 16384;
|
|
1720
|
-
uint32_t block_size = BT_BLOCK_SIZE;
|
|
1721
|
-
|
|
1722
|
-
for (uint32_t i = 0; i < dp.blocks_total; ++i) {
|
|
1723
|
-
BlockInfo block(dp.piece_index, i * block_size,
|
|
1724
|
-
(std::min)(block_size, piece_len - i * block_size));
|
|
1725
|
-
BlockState state = picker_->block_state(block);
|
|
1726
|
-
if (state == BlockState::Finished) {
|
|
1727
|
-
blocks.set_bit(i);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
data.unfinished_pieces[dp.piece_index] = blocks;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
// Statistics
|
|
1737
|
-
data.total_uploaded = stats_.total_uploaded;
|
|
1738
|
-
data.total_downloaded = stats_.total_downloaded;
|
|
1739
|
-
data.active_time = active_time_;
|
|
1740
|
-
data.seeding_time = seeding_time_;
|
|
1741
|
-
data.added_time = added_time_;
|
|
1742
|
-
data.completed_time = completed_time_;
|
|
1743
|
-
|
|
1744
|
-
// Configuration
|
|
1745
|
-
data.sequential_download = config_.sequential_download;
|
|
1746
|
-
data.max_connections = static_cast<int>(config_.max_connections);
|
|
1747
|
-
data.max_uploads = static_cast<int>(config_.max_uploads);
|
|
1748
|
-
data.download_limit = static_cast<int64_t>(config_.download_limit);
|
|
1749
|
-
data.upload_limit = static_cast<int64_t>(config_.upload_limit);
|
|
1750
|
-
|
|
1751
|
-
// Peer cache (save connected peers for quick reconnection)
|
|
1752
|
-
for (const auto& conn : connections_) {
|
|
1753
|
-
if (conn->is_connected()) {
|
|
1754
|
-
data.peers.emplace_back(conn->ip(), conn->port());
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
// Save info_dict (metadata) so we don't need to re-download it on restore
|
|
1759
|
-
// This is critical for seeding torrents - without it, the torrent would
|
|
1760
|
-
// enter DownloadingMetadata state and show 0% until metadata is re-fetched
|
|
1761
|
-
if (info_ && info_->has_metadata()) {
|
|
1762
|
-
const auto& info_bytes = info_->info_dict_bytes();
|
|
1763
|
-
if (!info_bytes.empty()) {
|
|
1764
|
-
data.info_dict = info_bytes;
|
|
1765
|
-
LOG_DEBUG("Torrent", "Saving info_dict in resume data: " +
|
|
1766
|
-
std::to_string(info_bytes.size()) + " bytes");
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
LOG_DEBUG("Torrent", "Generated resume data: " + std::to_string(data.have_pieces.count()) +
|
|
1771
|
-
"/" + std::to_string(data.have_pieces.size()) + " pieces, " +
|
|
1772
|
-
std::to_string(data.unfinished_pieces.size()) + " partial");
|
|
1773
|
-
|
|
1774
|
-
return data;
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
bool Torrent::save_resume_data() {
|
|
1778
|
-
std::string path;
|
|
1779
|
-
{
|
|
1780
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
1781
|
-
path = get_resume_file_path(config_.get_resume_path(), info_hash_);
|
|
1782
|
-
}
|
|
1783
|
-
return save_resume_data(path);
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
bool Torrent::save_resume_data(const std::string& path) {
|
|
1787
|
-
auto data = generate_resume_data();
|
|
1788
|
-
|
|
1789
|
-
if (!write_resume_data_file(data, path)) {
|
|
1790
|
-
LOG_ERROR("Torrent", "Failed to save resume data to " + path);
|
|
1791
|
-
return false;
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
LOG_INFO("Torrent", "Saved resume data for '" + name_ + "' to " + path);
|
|
1795
|
-
return true;
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
bool Torrent::load_resume_data(const TorrentResumeData& resume_data) {
|
|
1799
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
1800
|
-
|
|
1801
|
-
// Verify info hash matches
|
|
1802
|
-
if (resume_data.info_hash != info_hash_) {
|
|
1803
|
-
LOG_ERROR("Torrent", "Resume data info_hash mismatch");
|
|
1804
|
-
return false;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// If we don't have metadata but resume data has info_dict, restore it first
|
|
1808
|
-
// This is critical for seeding torrents - without metadata, torrent shows 0%
|
|
1809
|
-
if (!has_metadata_unlocked() && !resume_data.info_dict.empty()) {
|
|
1810
|
-
LOG_INFO("Torrent", "Restoring metadata from resume data (" +
|
|
1811
|
-
std::to_string(resume_data.info_dict.size()) + " bytes)");
|
|
1812
|
-
|
|
1813
|
-
auto new_info = TorrentInfo::from_info_dict(resume_data.info_dict, info_hash_);
|
|
1814
|
-
if (new_info) {
|
|
1815
|
-
info_ = std::make_unique<TorrentInfo>(*new_info);
|
|
1816
|
-
name_ = info_->name();
|
|
1817
|
-
|
|
1818
|
-
// Initialize picker now that we have metadata
|
|
1819
|
-
have_pieces_ = Bitfield(info_->num_pieces());
|
|
1820
|
-
uint32_t last_piece_len = info_->piece_size(info_->num_pieces() - 1);
|
|
1821
|
-
picker_ = std::make_unique<PiecePicker>(
|
|
1822
|
-
info_->num_pieces(),
|
|
1823
|
-
info_->piece_length(),
|
|
1824
|
-
last_piece_len
|
|
1825
|
-
);
|
|
1826
|
-
|
|
1827
|
-
if (config_.sequential_download) {
|
|
1828
|
-
picker_->set_mode(PickerMode::Sequential);
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
stats_.total_size = info_->total_size();
|
|
1832
|
-
stats_.pieces_total = info_->num_pieces();
|
|
1833
|
-
|
|
1834
|
-
LOG_INFO("Torrent", "Metadata restored from resume data: '" + name_ +
|
|
1835
|
-
"', " + std::to_string(info_->num_pieces()) + " pieces");
|
|
1836
|
-
} else {
|
|
1837
|
-
LOG_WARN("Torrent", "Failed to parse info_dict from resume data");
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
// Apply have pieces
|
|
1842
|
-
if (resume_data.have_pieces.size() > 0) {
|
|
1843
|
-
if (picker_) {
|
|
1844
|
-
// Update picker with have pieces
|
|
1845
|
-
for (uint32_t i = 0; i < resume_data.have_pieces.size(); ++i) {
|
|
1846
|
-
if (resume_data.have_pieces.get_bit(i)) {
|
|
1847
|
-
picker_->mark_have(i);
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
have_pieces_ = picker_->get_have_bitfield();
|
|
1851
|
-
} else {
|
|
1852
|
-
// No picker yet (metadata not received), store for later
|
|
1853
|
-
// When metadata arrives, set_metadata() will use this
|
|
1854
|
-
have_pieces_ = resume_data.have_pieces;
|
|
1855
|
-
LOG_DEBUG("Torrent", "Stored " + std::to_string(resume_data.have_pieces.count()) +
|
|
1856
|
-
" pieces for when metadata arrives");
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
LOG_INFO("Torrent", "Loaded resume data: " +
|
|
1860
|
-
std::to_string(resume_data.have_pieces.count()) + " pieces");
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// Apply unfinished pieces
|
|
1864
|
-
// NOTE: For now, we don't restore partial pieces - they will be re-downloaded
|
|
1865
|
-
// This is safer than trying to restore partial block state
|
|
1866
|
-
// A future optimization could verify the existing blocks on disk
|
|
1867
|
-
if (!resume_data.unfinished_pieces.empty()) {
|
|
1868
|
-
LOG_DEBUG("Torrent", "Resume data has " +
|
|
1869
|
-
std::to_string(resume_data.unfinished_pieces.size()) +
|
|
1870
|
-
" partial pieces (will be re-downloaded)");
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
// Restore statistics
|
|
1874
|
-
stats_.total_uploaded = resume_data.total_uploaded;
|
|
1875
|
-
stats_.total_downloaded = resume_data.total_downloaded;
|
|
1876
|
-
active_time_ = resume_data.active_time;
|
|
1877
|
-
seeding_time_ = resume_data.seeding_time;
|
|
1878
|
-
added_time_ = resume_data.added_time;
|
|
1879
|
-
completed_time_ = resume_data.completed_time;
|
|
1880
|
-
|
|
1881
|
-
// Restore configuration
|
|
1882
|
-
if (resume_data.max_connections > 0) {
|
|
1883
|
-
config_.max_connections = static_cast<size_t>(resume_data.max_connections);
|
|
1884
|
-
}
|
|
1885
|
-
if (resume_data.max_uploads > 0) {
|
|
1886
|
-
config_.max_uploads = static_cast<size_t>(resume_data.max_uploads);
|
|
1887
|
-
}
|
|
1888
|
-
if (resume_data.download_limit > 0) {
|
|
1889
|
-
config_.download_limit = static_cast<uint64_t>(resume_data.download_limit);
|
|
1890
|
-
}
|
|
1891
|
-
if (resume_data.upload_limit > 0) {
|
|
1892
|
-
config_.upload_limit = static_cast<uint64_t>(resume_data.upload_limit);
|
|
1893
|
-
}
|
|
1894
|
-
config_.sequential_download = resume_data.sequential_download;
|
|
1895
|
-
if (picker_ && config_.sequential_download) {
|
|
1896
|
-
picker_->set_mode(PickerMode::Sequential);
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
// Add cached peers
|
|
1900
|
-
for (const auto& [ip, port] : resume_data.peers) {
|
|
1901
|
-
pending_peers_.emplace_back(ip, port);
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
// Update stats
|
|
1905
|
-
if (picker_) {
|
|
1906
|
-
stats_.pieces_done = picker_->num_have();
|
|
1907
|
-
stats_.bytes_done = 0;
|
|
1908
|
-
if (info_) {
|
|
1909
|
-
// Loop through ALL pieces and check if we have each one
|
|
1910
|
-
for (uint32_t i = 0; i < have_pieces_.size(); ++i) {
|
|
1911
|
-
if (have_pieces_.get_bit(i)) {
|
|
1912
|
-
stats_.bytes_done += info_->piece_size(i);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
stats_.progress = static_cast<float>(stats_.pieces_done) /
|
|
1917
|
-
static_cast<float>(stats_.pieces_total);
|
|
1918
|
-
LOG_INFO("Torrent", "Resume data applied: " + std::to_string(stats_.pieces_done) +
|
|
1919
|
-
"/" + std::to_string(stats_.pieces_total) + " pieces (" +
|
|
1920
|
-
std::to_string(stats_.bytes_done) + " bytes)");
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
return true;
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
bool Torrent::try_load_resume_data() {
|
|
1927
|
-
std::string path;
|
|
1928
|
-
{
|
|
1929
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
1930
|
-
path = get_resume_file_path(config_.get_resume_path(), info_hash_);
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
std::string error;
|
|
1934
|
-
auto resume_data = read_resume_data_file(path, &error);
|
|
1935
|
-
|
|
1936
|
-
if (!resume_data) {
|
|
1937
|
-
LOG_DEBUG("Torrent", "No resume data found for '" + name_ + "': " + error);
|
|
1938
|
-
return false;
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
return load_resume_data(*resume_data);
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
//=============================================================================
|
|
1945
|
-
// File Verification (Recheck)
|
|
1946
|
-
//=============================================================================
|
|
1947
|
-
|
|
1948
|
-
void Torrent::verify_piece_hash_sync(uint32_t piece, bool& valid) {
|
|
1949
|
-
// Note: This reads the piece synchronously and computes hash
|
|
1950
|
-
// For use during file checking, not during normal download
|
|
1951
|
-
if (!info_) {
|
|
1952
|
-
valid = false;
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
uint32_t actual_length = info_->piece_size(piece);
|
|
1957
|
-
auto mappings = get_file_mappings();
|
|
1958
|
-
|
|
1959
|
-
// Calculate piece offset in torrent
|
|
1960
|
-
uint64_t piece_offset = static_cast<uint64_t>(piece) * info_->piece_length();
|
|
1961
|
-
|
|
1962
|
-
// Read the piece data
|
|
1963
|
-
std::vector<uint8_t> piece_data(actual_length);
|
|
1964
|
-
size_t bytes_read = 0;
|
|
1965
|
-
|
|
1966
|
-
// Map to files and read
|
|
1967
|
-
for (const auto& mapping : mappings) {
|
|
1968
|
-
if (bytes_read >= actual_length) break;
|
|
1969
|
-
|
|
1970
|
-
// Check if this file contains part of our piece
|
|
1971
|
-
uint64_t file_start = mapping.torrent_offset;
|
|
1972
|
-
uint64_t file_end = file_start + mapping.length;
|
|
1973
|
-
|
|
1974
|
-
if (piece_offset + bytes_read >= file_end) continue;
|
|
1975
|
-
if (piece_offset + actual_length <= file_start) continue;
|
|
1976
|
-
|
|
1977
|
-
// Calculate read range within this file
|
|
1978
|
-
uint64_t read_start = 0;
|
|
1979
|
-
uint64_t read_offset_in_file = 0;
|
|
1980
|
-
|
|
1981
|
-
if (piece_offset + bytes_read >= file_start) {
|
|
1982
|
-
read_offset_in_file = piece_offset + bytes_read - file_start;
|
|
1983
|
-
} else {
|
|
1984
|
-
read_start = file_start - piece_offset;
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
size_t read_len = static_cast<size_t>(
|
|
1988
|
-
(std::min)(static_cast<uint64_t>(actual_length - bytes_read),
|
|
1989
|
-
file_end - (file_start + read_offset_in_file)));
|
|
1990
|
-
|
|
1991
|
-
std::string file_path = combine_paths(config_.save_path, mapping.path);
|
|
1992
|
-
|
|
1993
|
-
if (!file_exists(file_path)) {
|
|
1994
|
-
valid = false;
|
|
1995
|
-
return;
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
if (!read_file_chunk(file_path, read_offset_in_file,
|
|
1999
|
-
piece_data.data() + read_start, read_len)) {
|
|
2000
|
-
valid = false;
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
bytes_read += read_len;
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
if (bytes_read < actual_length) {
|
|
2008
|
-
valid = false;
|
|
2009
|
-
return;
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
// Compute SHA1 hash
|
|
2013
|
-
SHA1 sha1;
|
|
2014
|
-
sha1.update(piece_data.data(), piece_data.size());
|
|
2015
|
-
std::string calculated_hash = sha1.finalize();
|
|
2016
|
-
|
|
2017
|
-
// Compare with expected hash
|
|
2018
|
-
auto expected_hash = info_->piece_hash(piece);
|
|
2019
|
-
std::string expected_hex;
|
|
2020
|
-
for (uint8_t b : expected_hash) {
|
|
2021
|
-
char hex[3];
|
|
2022
|
-
snprintf(hex, sizeof(hex), "%02x", b);
|
|
2023
|
-
expected_hex += hex;
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
valid = (calculated_hash == expected_hex);
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
void Torrent::check_files(std::function<void(uint32_t, uint32_t)> progress_callback) {
|
|
2030
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
2031
|
-
|
|
2032
|
-
if (!info_ || !picker_) {
|
|
2033
|
-
LOG_WARN("Torrent", "Cannot check files - no metadata");
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
LOG_INFO("Torrent", "Checking files for '" + name_ + "'...");
|
|
2038
|
-
|
|
2039
|
-
uint32_t num_pieces = info_->num_pieces();
|
|
2040
|
-
uint32_t pieces_have = 0;
|
|
2041
|
-
|
|
2042
|
-
// Reset bitfield
|
|
2043
|
-
have_pieces_ = Bitfield(num_pieces);
|
|
2044
|
-
picker_->set_have_bitfield(have_pieces_);
|
|
2045
|
-
|
|
2046
|
-
for (uint32_t i = 0; i < num_pieces; ++i) {
|
|
2047
|
-
bool valid = false;
|
|
2048
|
-
verify_piece_hash_sync(i, valid);
|
|
2049
|
-
|
|
2050
|
-
if (valid) {
|
|
2051
|
-
picker_->mark_have(i);
|
|
2052
|
-
have_pieces_.set_bit(i);
|
|
2053
|
-
++pieces_have;
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
if (progress_callback) {
|
|
2057
|
-
progress_callback(i + 1, num_pieces);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
// Update stats
|
|
2062
|
-
stats_.pieces_done = pieces_have;
|
|
2063
|
-
stats_.bytes_done = 0;
|
|
2064
|
-
for (uint32_t i = 0; i < num_pieces; ++i) {
|
|
2065
|
-
if (have_pieces_.get_bit(i)) {
|
|
2066
|
-
stats_.bytes_done += info_->piece_size(i);
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
stats_.progress = static_cast<float>(pieces_have) / static_cast<float>(num_pieces);
|
|
2070
|
-
|
|
2071
|
-
LOG_INFO("Torrent", "File check complete: " + std::to_string(pieces_have) +
|
|
2072
|
-
"/" + std::to_string(num_pieces) + " pieces verified");
|
|
2073
|
-
|
|
2074
|
-
// If complete, switch to seeding
|
|
2075
|
-
if (picker_->is_complete()) {
|
|
2076
|
-
set_state(TorrentState::Seeding);
|
|
2077
|
-
choker_.set_seed_mode(true);
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
void Torrent::check_files_async(
|
|
2082
|
-
std::function<void(uint32_t, uint32_t)> completion_callback,
|
|
2083
|
-
std::function<void(uint32_t, uint32_t)> progress_callback) {
|
|
2084
|
-
|
|
2085
|
-
// Run file check in a separate thread
|
|
2086
|
-
auto weak_self = weak_from_this();
|
|
2087
|
-
|
|
2088
|
-
std::thread([weak_self, completion_callback, progress_callback]() {
|
|
2089
|
-
auto self = weak_self.lock();
|
|
2090
|
-
if (!self) return;
|
|
2091
|
-
|
|
2092
|
-
self->check_files(progress_callback);
|
|
2093
|
-
|
|
2094
|
-
uint32_t have = 0, total = 0;
|
|
2095
|
-
{
|
|
2096
|
-
std::lock_guard<std::mutex> lock(self->mutex_);
|
|
2097
|
-
have = self->stats_.pieces_done;
|
|
2098
|
-
total = self->stats_.pieces_total;
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
if (completion_callback) {
|
|
2102
|
-
completion_callback(have, total);
|
|
2103
|
-
}
|
|
2104
|
-
}).detach();
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
void Torrent::on_file_check_complete(uint32_t pieces_have) {
|
|
2108
|
-
std::lock_guard<std::mutex> lock(mutex_);
|
|
2109
|
-
|
|
2110
|
-
LOG_INFO("Torrent", "File check complete: " + std::to_string(pieces_have) + " pieces");
|
|
2111
|
-
|
|
2112
|
-
if (picker_ && picker_->is_complete()) {
|
|
2113
|
-
set_state(TorrentState::Seeding);
|
|
2114
|
-
choker_.set_seed_mode(true);
|
|
2115
|
-
} else if (state_ == TorrentState::CheckingFiles) {
|
|
2116
|
-
set_state(TorrentState::Downloading);
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
} // namespace librats
|