librats 0.8.0 → 0.9.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.
@@ -3,9 +3,9 @@
3
3
 
4
4
  namespace librats {
5
5
 
6
- //=============================================================================
7
- // File Transfer API Implementation
8
- //=============================================================================
6
+ // =============================================================================
7
+ // File transfer API - thin forwarding layer over FileTransferManager
8
+ // =============================================================================
9
9
 
10
10
  FileTransferManager& RatsClient::get_file_transfer_manager() {
11
11
  if (!file_transfer_manager_) {
@@ -18,24 +18,22 @@ bool RatsClient::is_file_transfer_available() const {
18
18
  return file_transfer_manager_ != nullptr;
19
19
  }
20
20
 
21
- std::string RatsClient::send_file(const std::string& peer_id, const std::string& file_path,
22
- const std::string& remote_filename) {
21
+ std::string RatsClient::send_file(const std::string& peer_id, const std::string& file_path,
22
+ const std::string& remote_filename) {
23
23
  if (!is_file_transfer_available()) {
24
24
  LOG_CLIENT_ERROR("File transfer manager not available");
25
25
  return "";
26
26
  }
27
-
28
27
  return file_transfer_manager_->send_file(peer_id, file_path, remote_filename);
29
28
  }
30
29
 
31
30
  std::string RatsClient::send_directory(const std::string& peer_id, const std::string& directory_path,
32
- const std::string& remote_directory_name, bool recursive) {
31
+ const std::string& remote_name) {
33
32
  if (!is_file_transfer_available()) {
34
33
  LOG_CLIENT_ERROR("File transfer manager not available");
35
34
  return "";
36
35
  }
37
-
38
- return file_transfer_manager_->send_directory(peer_id, directory_path, remote_directory_name, recursive);
36
+ return file_transfer_manager_->send_directory(peer_id, directory_path, remote_name);
39
37
  }
40
38
 
41
39
  bool RatsClient::accept_file_transfer(const std::string& transfer_id, const std::string& local_path) {
@@ -43,8 +41,7 @@ bool RatsClient::accept_file_transfer(const std::string& transfer_id, const std:
43
41
  LOG_CLIENT_ERROR("File transfer manager not available");
44
42
  return false;
45
43
  }
46
-
47
- return file_transfer_manager_->accept_file_transfer(transfer_id, local_path);
44
+ return file_transfer_manager_->accept(transfer_id, local_path);
48
45
  }
49
46
 
50
47
  bool RatsClient::reject_file_transfer(const std::string& transfer_id, const std::string& reason) {
@@ -52,26 +49,7 @@ bool RatsClient::reject_file_transfer(const std::string& transfer_id, const std:
52
49
  LOG_CLIENT_ERROR("File transfer manager not available");
53
50
  return false;
54
51
  }
55
-
56
- return file_transfer_manager_->reject_file_transfer(transfer_id, reason);
57
- }
58
-
59
- bool RatsClient::accept_directory_transfer(const std::string& transfer_id, const std::string& local_path) {
60
- if (!is_file_transfer_available()) {
61
- LOG_CLIENT_ERROR("File transfer manager not available");
62
- return false;
63
- }
64
-
65
- return file_transfer_manager_->accept_directory_transfer(transfer_id, local_path);
66
- }
67
-
68
- bool RatsClient::reject_directory_transfer(const std::string& transfer_id, const std::string& reason) {
69
- if (!is_file_transfer_available()) {
70
- LOG_CLIENT_ERROR("File transfer manager not available");
71
- return false;
72
- }
73
-
74
- return file_transfer_manager_->reject_directory_transfer(transfer_id, reason);
52
+ return file_transfer_manager_->reject(transfer_id, reason);
75
53
  }
76
54
 
77
55
  bool RatsClient::pause_file_transfer(const std::string& transfer_id) {
@@ -79,8 +57,7 @@ bool RatsClient::pause_file_transfer(const std::string& transfer_id) {
79
57
  LOG_CLIENT_ERROR("File transfer manager not available");
80
58
  return false;
81
59
  }
82
-
83
- return file_transfer_manager_->pause_transfer(transfer_id);
60
+ return file_transfer_manager_->pause(transfer_id);
84
61
  }
85
62
 
86
63
  bool RatsClient::resume_file_transfer(const std::string& transfer_id) {
@@ -88,8 +65,7 @@ bool RatsClient::resume_file_transfer(const std::string& transfer_id) {
88
65
  LOG_CLIENT_ERROR("File transfer manager not available");
89
66
  return false;
90
67
  }
91
-
92
- return file_transfer_manager_->resume_transfer(transfer_id);
68
+ return file_transfer_manager_->resume(transfer_id);
93
69
  }
94
70
 
95
71
  bool RatsClient::cancel_file_transfer(const std::string& transfer_id) {
@@ -97,25 +73,24 @@ bool RatsClient::cancel_file_transfer(const std::string& transfer_id) {
97
73
  LOG_CLIENT_ERROR("File transfer manager not available");
98
74
  return false;
99
75
  }
100
-
101
- return file_transfer_manager_->cancel_transfer(transfer_id);
76
+ return file_transfer_manager_->cancel(transfer_id);
102
77
  }
103
78
 
104
- std::shared_ptr<FileTransferProgress> RatsClient::get_file_transfer_progress(const std::string& transfer_id) const {
79
+ std::shared_ptr<FileTransferProgress>
80
+ RatsClient::get_file_transfer_progress(const std::string& transfer_id) const {
105
81
  if (!is_file_transfer_available()) {
106
82
  LOG_CLIENT_ERROR("File transfer manager not available");
107
83
  return nullptr;
108
84
  }
109
-
110
- return file_transfer_manager_->get_transfer_progress(transfer_id);
85
+ return file_transfer_manager_->get_progress(transfer_id);
111
86
  }
112
87
 
113
- std::vector<std::shared_ptr<FileTransferProgress>> RatsClient::get_active_file_transfers() const {
88
+ std::vector<std::shared_ptr<FileTransferProgress>>
89
+ RatsClient::get_active_file_transfers() const {
114
90
  if (!is_file_transfer_available()) {
115
91
  LOG_CLIENT_ERROR("File transfer manager not available");
116
92
  return {};
117
93
  }
118
-
119
94
  return file_transfer_manager_->get_active_transfers();
120
95
  }
121
96
 
@@ -124,8 +99,7 @@ nlohmann::json RatsClient::get_file_transfer_statistics() const {
124
99
  LOG_CLIENT_ERROR("File transfer manager not available");
125
100
  return nlohmann::json::object();
126
101
  }
127
-
128
- return file_transfer_manager_->get_transfer_statistics();
102
+ return file_transfer_manager_->get_statistics();
129
103
  }
130
104
 
131
105
  void RatsClient::set_file_transfer_config(const FileTransferConfig& config) {
@@ -133,7 +107,6 @@ void RatsClient::set_file_transfer_config(const FileTransferConfig& config) {
133
107
  LOG_CLIENT_ERROR("File transfer manager not available");
134
108
  return;
135
109
  }
136
-
137
110
  file_transfer_manager_->set_config(config);
138
111
  }
139
112
 
@@ -141,82 +114,31 @@ FileTransferConfig RatsClient::get_file_transfer_config() const {
141
114
  if (!is_file_transfer_available()) {
142
115
  throw std::runtime_error("File transfer manager not available");
143
116
  }
144
-
145
117
  return file_transfer_manager_->get_config();
146
118
  }
147
119
 
148
- void RatsClient::on_file_transfer_progress(FileTransferProgressCallback callback) {
149
- if (!is_file_transfer_available()) {
150
- LOG_CLIENT_ERROR("File transfer manager not available");
151
- return;
152
- }
153
-
154
- file_transfer_manager_->set_progress_callback(callback);
155
- }
156
-
157
- void RatsClient::on_file_transfer_completed(FileTransferCompletedCallback callback) {
158
- if (!is_file_transfer_available()) {
159
- LOG_CLIENT_ERROR("File transfer manager not available");
160
- return;
161
- }
162
-
163
- file_transfer_manager_->set_completion_callback(callback);
164
- }
165
-
166
- void RatsClient::on_file_transfer_request(FileTransferRequestCallback callback) {
120
+ void RatsClient::on_file_transfer_progress(TransferProgressCallback callback) {
167
121
  if (!is_file_transfer_available()) {
168
122
  LOG_CLIENT_ERROR("File transfer manager not available");
169
123
  return;
170
124
  }
171
-
172
- file_transfer_manager_->set_request_callback(callback);
173
- }
174
-
175
- void RatsClient::on_directory_transfer_progress(DirectoryTransferProgressCallback callback) {
176
- if (!is_file_transfer_available()) {
177
- LOG_CLIENT_ERROR("File transfer manager not available");
178
- return;
179
- }
180
-
181
- file_transfer_manager_->set_directory_progress_callback(callback);
182
- }
183
-
184
- std::string RatsClient::request_file(const std::string& peer_id, const std::string& remote_file_path,
185
- const std::string& local_path) {
186
- if (!is_file_transfer_available()) {
187
- LOG_CLIENT_ERROR("File transfer manager not available");
188
- return "";
189
- }
190
-
191
- return file_transfer_manager_->request_file(peer_id, remote_file_path, local_path);
192
- }
193
-
194
- std::string RatsClient::request_directory(const std::string& peer_id, const std::string& remote_directory_path,
195
- const std::string& local_directory_path, bool recursive) {
196
- if (!is_file_transfer_available()) {
197
- LOG_CLIENT_ERROR("File transfer manager not available");
198
- return "";
199
- }
200
-
201
- return file_transfer_manager_->request_directory(peer_id, remote_directory_path, local_directory_path, recursive);
125
+ file_transfer_manager_->set_progress_callback(std::move(callback));
202
126
  }
203
127
 
204
- void RatsClient::on_file_request(FileRequestCallback callback) {
128
+ void RatsClient::on_file_transfer_completed(TransferCompletedCallback callback) {
205
129
  if (!is_file_transfer_available()) {
206
130
  LOG_CLIENT_ERROR("File transfer manager not available");
207
131
  return;
208
132
  }
209
-
210
- file_transfer_manager_->set_file_request_callback(callback);
133
+ file_transfer_manager_->set_completed_callback(std::move(callback));
211
134
  }
212
135
 
213
- void RatsClient::on_directory_request(DirectoryRequestCallback callback) {
136
+ void RatsClient::on_file_transfer_request(TransferOfferCallback callback) {
214
137
  if (!is_file_transfer_available()) {
215
138
  LOG_CLIENT_ERROR("File transfer manager not available");
216
139
  return;
217
140
  }
218
-
219
- file_transfer_manager_->set_directory_request_callback(callback);
141
+ file_transfer_manager_->set_offer_callback(std::move(callback));
220
142
  }
221
143
 
222
- }
144
+ } // namespace librats
@@ -91,6 +91,10 @@ std::optional<StunMappedAddress> RatsClient::discover_public_address(
91
91
 
92
92
  if (result.success && result.mapped_address) {
93
93
  LOG_INFO("ice", "Discovered public address: " << result.mapped_address->to_string());
94
+
95
+ // Add public IP to our own addresses list to prevent self-connections
96
+ add_ignored_address(result.mapped_address->address);
97
+
94
98
  return result.mapped_address;
95
99
  }
96
100
 
@@ -24,10 +24,18 @@ nlohmann::json RatsClient::get_connection_statistics() const {
24
24
  stats["our_peer_id"] = get_our_peer_id();
25
25
  stats["encryption_enabled"] = is_encryption_enabled();
26
26
 
27
- // DHT statistics
28
- if (dht_client_ && dht_client_->is_running()) {
27
+ // DHT statistics (IPv4 + IPv6 combined)
28
+ if (is_dht_running()) {
29
29
  stats["dht_running"] = true;
30
30
  stats["dht_routing_table_size"] = get_dht_routing_table_size();
31
+ stats["dht_ipv4_running"] = (dht_client_ && dht_client_->is_running());
32
+ stats["dht_ipv6_running"] = (dht_client_v6_ && dht_client_v6_->is_running());
33
+ if (dht_client_ && dht_client_->is_running()) {
34
+ stats["dht_ipv4_routing_table_size"] = dht_client_->get_routing_table_size();
35
+ }
36
+ if (dht_client_v6_ && dht_client_v6_->is_running()) {
37
+ stats["dht_ipv6_routing_table_size"] = dht_client_v6_->get_routing_table_size();
38
+ }
31
39
  } else {
32
40
  stats["dht_running"] = false;
33
41
  }
@@ -760,13 +760,25 @@ static bool build_udp_dest_addr(const std::string& host, int port, AddressFamily
760
760
  sockaddr_storage& addr, socklen_t& addr_len) {
761
761
  memset(&addr, 0, sizeof(addr));
762
762
 
763
- // Native IPv6 address
763
+ // Native IPv6 address, or hostname that must resolve to IPv6 (pure IPv6 socket).
764
+ // A pure IPv6 socket (V6ONLY) cannot send to IPv4-mapped addresses, so hostnames
765
+ // are resolved via AAAA records here.
766
+ std::string ipv6_host;
764
767
  if (network_utils::is_valid_ipv6(host)) {
768
+ ipv6_host = host;
769
+ } else if (af == AddressFamily::IPv6) {
770
+ ipv6_host = network_utils::resolve_hostname_v6(host);
771
+ if (ipv6_host.empty()) {
772
+ LOG_SOCKET_DEBUG("Failed to resolve hostname to IPv6: " << host);
773
+ return false;
774
+ }
775
+ }
776
+ if (!ipv6_host.empty()) {
765
777
  auto* a6 = reinterpret_cast<sockaddr_in6*>(&addr);
766
778
  a6->sin6_family = AF_INET6;
767
779
  a6->sin6_port = htons(port);
768
- if (inet_pton(AF_INET6, host.c_str(), &a6->sin6_addr) <= 0) {
769
- LOG_SOCKET_ERROR("Invalid IPv6 address: " << host);
780
+ if (inet_pton(AF_INET6, ipv6_host.c_str(), &a6->sin6_addr) <= 0) {
781
+ LOG_SOCKET_ERROR("Invalid IPv6 address: " << ipv6_host);
770
782
  return false;
771
783
  }
772
784
  addr_len = sizeof(sockaddr_in6);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librats",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Node.js bindings for librats - A high-performance peer-to-peer networking library",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -136,6 +136,9 @@ try {
136
136
  '-DRATS_BUILD_EXAMPLES=OFF',
137
137
  '-DRATS_STATIC_LIBRARY=ON',
138
138
  '-DRATS_BINDINGS=ON',
139
+ // The bindings only need the static archive; install/package-config rules
140
+ // (find_package support) are unnecessary here and pull in cmake/ratsConfig.cmake.in.
141
+ '-DRATS_INSTALL=OFF',
139
142
  '-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL'
140
143
  ];
141
144
 
@@ -108,6 +108,16 @@ copyDir(
108
108
  srcFilter
109
109
  );
110
110
 
111
+ // Copy cmake/ directory (package-config templates referenced by CMakeLists.txt
112
+ // when RATS_INSTALL is enabled, e.g. cmake/ratsConfig.cmake.in)
113
+ if (fs.existsSync(path.join(projectRoot, 'cmake'))) {
114
+ console.log(' Copying cmake/ directory...');
115
+ copyDir(
116
+ path.join(projectRoot, 'cmake'),
117
+ path.join(nativeSrcDir, 'cmake')
118
+ );
119
+ }
120
+
111
121
  // Copy 3rdparty/ directory
112
122
  if (fs.existsSync(path.join(projectRoot, '3rdparty'))) {
113
123
  console.log(' Copying 3rdparty/ directory...');
@@ -24,6 +24,7 @@ std::unordered_map<rats_client_t, std::shared_ptr<CallbackData>> binary_callback
24
24
  std::unordered_map<rats_client_t, std::shared_ptr<CallbackData>> json_callbacks;
25
25
  std::unordered_map<rats_client_t, std::shared_ptr<CallbackData>> disconnect_callbacks;
26
26
  std::unordered_map<rats_client_t, std::shared_ptr<CallbackData>> file_progress_callbacks;
27
+ std::unordered_map<rats_client_t, std::shared_ptr<CallbackData>> file_request_callbacks;
27
28
 
28
29
  // C callback wrappers
29
30
  void connection_callback_wrapper(void* user_data, const char* peer_id) {
@@ -95,6 +96,23 @@ void file_progress_callback_wrapper(void* user_data, const char* transfer_id, in
95
96
  }
96
97
  }
97
98
 
99
+ // Fires for every incoming transfer offer (file or directory). The app should
100
+ // respond by calling acceptFileTransfer()/rejectFileTransfer(). remote_path is
101
+ // always empty in the push-only model, so only filename is forwarded.
102
+ void file_request_callback_wrapper(void* user_data, const char* peer_id, const char* transfer_id, const char* remote_path, const char* filename) {
103
+ (void)remote_path;
104
+ rats_client_t client = static_cast<rats_client_t>(user_data);
105
+ auto it = file_request_callbacks.find(client);
106
+ if (it != file_request_callbacks.end() && it->second) {
107
+ auto callback_data = it->second;
108
+ callback_data->callback.Call({
109
+ Napi::String::New(callback_data->env, peer_id),
110
+ Napi::String::New(callback_data->env, transfer_id),
111
+ Napi::String::New(callback_data->env, filename)
112
+ });
113
+ }
114
+ }
115
+
98
116
  class RatsClient : public Napi::ObjectWrap<RatsClient> {
99
117
  public:
100
118
  static Napi::Object Init(Napi::Env env, Napi::Object exports) {
@@ -178,8 +196,6 @@ public:
178
196
  // File Transfer methods
179
197
  InstanceMethod("sendFile", &RatsClient::SendFile),
180
198
  InstanceMethod("sendDirectory", &RatsClient::SendDirectory),
181
- InstanceMethod("requestFile", &RatsClient::RequestFile),
182
- InstanceMethod("requestDirectory", &RatsClient::RequestDirectory),
183
199
  InstanceMethod("acceptFileTransfer", &RatsClient::AcceptFileTransfer),
184
200
  InstanceMethod("rejectFileTransfer", &RatsClient::RejectFileTransfer),
185
201
  InstanceMethod("cancelFileTransfer", &RatsClient::CancelFileTransfer),
@@ -198,6 +214,7 @@ public:
198
214
  InstanceMethod("onJson", &RatsClient::OnJson),
199
215
  InstanceMethod("onDisconnect", &RatsClient::OnDisconnect),
200
216
  InstanceMethod("onFileProgress", &RatsClient::OnFileProgress),
217
+ InstanceMethod("onFileRequest", &RatsClient::OnFileRequest),
201
218
 
202
219
  // Configuration persistence
203
220
  InstanceMethod("loadConfiguration", &RatsClient::LoadConfiguration),
@@ -240,7 +257,8 @@ public:
240
257
  json_callbacks.erase(client_);
241
258
  disconnect_callbacks.erase(client_);
242
259
  file_progress_callbacks.erase(client_);
243
-
260
+ file_request_callbacks.erase(client_);
261
+
244
262
  rats_destroy(client_);
245
263
  }
246
264
  }
@@ -542,9 +560,10 @@ private:
542
560
  Napi::Value StartMdnsDiscovery(const Napi::CallbackInfo& info) {
543
561
  Napi::Env env = info.Env();
544
562
 
563
+ std::string service;
545
564
  const char* service_name = nullptr;
546
565
  if (info.Length() > 0 && info[0].IsString()) {
547
- std::string service = info[0].As<Napi::String>().Utf8Value();
566
+ service = info[0].As<Napi::String>().Utf8Value();
548
567
  service_name = service.c_str();
549
568
  }
550
569
 
@@ -750,12 +769,13 @@ private:
750
769
  Napi::Value DiscoverPublicAddress(const Napi::CallbackInfo& info) {
751
770
  Napi::Env env = info.Env();
752
771
 
772
+ std::string server_str;
753
773
  const char* server = nullptr;
754
774
  uint16_t port = 0;
755
775
  int timeout_ms = 5000;
756
-
776
+
757
777
  if (info.Length() > 0 && info[0].IsString()) {
758
- std::string server_str = info[0].As<Napi::String>().Utf8Value();
778
+ server_str = info[0].As<Napi::String>().Utf8Value();
759
779
  server = server_str.c_str();
760
780
  }
761
781
  if (info.Length() > 1 && info[1].IsNumber()) {
@@ -976,12 +996,13 @@ private:
976
996
  std::string peer_id = info[0].As<Napi::String>().Utf8Value();
977
997
  std::string file_path = info[1].As<Napi::String>().Utf8Value();
978
998
 
999
+ std::string remote_name;
979
1000
  const char* remote_filename = nullptr;
980
1001
  if (info.Length() > 2 && info[2].IsString()) {
981
- std::string remote_name = info[2].As<Napi::String>().Utf8Value();
1002
+ remote_name = info[2].As<Napi::String>().Utf8Value();
982
1003
  remote_filename = remote_name.c_str();
983
1004
  }
984
-
1005
+
985
1006
  char* transfer_id = rats_send_file(client_, peer_id.c_str(), file_path.c_str(), remote_filename);
986
1007
  if (!transfer_id) return env.Null();
987
1008
 
@@ -1001,63 +1022,15 @@ private:
1001
1022
  std::string peer_id = info[0].As<Napi::String>().Utf8Value();
1002
1023
  std::string directory_path = info[1].As<Napi::String>().Utf8Value();
1003
1024
 
1025
+ std::string remote_name;
1004
1026
  const char* remote_directory_name = nullptr;
1005
1027
  if (info.Length() > 2 && info[2].IsString()) {
1006
- std::string remote_name = info[2].As<Napi::String>().Utf8Value();
1028
+ remote_name = info[2].As<Napi::String>().Utf8Value();
1007
1029
  remote_directory_name = remote_name.c_str();
1008
1030
  }
1009
-
1010
- int recursive = 1;
1011
- if (info.Length() > 3 && info[3].IsBoolean()) {
1012
- recursive = info[3].As<Napi::Boolean>().Value() ? 1 : 0;
1013
- }
1014
-
1015
- char* transfer_id = rats_send_directory(client_, peer_id.c_str(), directory_path.c_str(), remote_directory_name, recursive);
1016
- if (!transfer_id) return env.Null();
1017
-
1018
- Napi::String result = Napi::String::New(env, transfer_id);
1019
- rats_string_free(transfer_id);
1020
- return result;
1021
- }
1022
-
1023
- Napi::Value RequestFile(const Napi::CallbackInfo& info) {
1024
- Napi::Env env = info.Env();
1025
-
1026
- if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
1027
- Napi::TypeError::New(env, "Expected peer_id (string), remote_file_path (string), and local_path (string)").ThrowAsJavaScriptException();
1028
- return env.Null();
1029
- }
1030
-
1031
- std::string peer_id = info[0].As<Napi::String>().Utf8Value();
1032
- std::string remote_file_path = info[1].As<Napi::String>().Utf8Value();
1033
- std::string local_path = info[2].As<Napi::String>().Utf8Value();
1034
-
1035
- char* transfer_id = rats_request_file(client_, peer_id.c_str(), remote_file_path.c_str(), local_path.c_str());
1036
- if (!transfer_id) return env.Null();
1037
-
1038
- Napi::String result = Napi::String::New(env, transfer_id);
1039
- rats_string_free(transfer_id);
1040
- return result;
1041
- }
1042
-
1043
- Napi::Value RequestDirectory(const Napi::CallbackInfo& info) {
1044
- Napi::Env env = info.Env();
1045
-
1046
- if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
1047
- Napi::TypeError::New(env, "Expected peer_id (string), remote_directory_path (string), and local_directory_path (string)").ThrowAsJavaScriptException();
1048
- return env.Null();
1049
- }
1050
-
1051
- std::string peer_id = info[0].As<Napi::String>().Utf8Value();
1052
- std::string remote_directory_path = info[1].As<Napi::String>().Utf8Value();
1053
- std::string local_directory_path = info[2].As<Napi::String>().Utf8Value();
1054
-
1055
- int recursive = 1;
1056
- if (info.Length() > 3 && info[3].IsBoolean()) {
1057
- recursive = info[3].As<Napi::Boolean>().Value() ? 1 : 0;
1058
- }
1059
-
1060
- char* transfer_id = rats_request_directory(client_, peer_id.c_str(), remote_directory_path.c_str(), local_directory_path.c_str(), recursive);
1031
+
1032
+ // Directory transfers are always recursive; the trailing flag is ignored.
1033
+ char* transfer_id = rats_send_directory(client_, peer_id.c_str(), directory_path.c_str(), remote_directory_name, 1);
1061
1034
  if (!transfer_id) return env.Null();
1062
1035
 
1063
1036
  Napi::String result = Napi::String::New(env, transfer_id);
@@ -1090,12 +1063,13 @@ private:
1090
1063
 
1091
1064
  std::string transfer_id = info[0].As<Napi::String>().Utf8Value();
1092
1065
 
1066
+ std::string reason_str;
1093
1067
  const char* reason = nullptr;
1094
1068
  if (info.Length() > 1 && info[1].IsString()) {
1095
- std::string reason_str = info[1].As<Napi::String>().Utf8Value();
1069
+ reason_str = info[1].As<Napi::String>().Utf8Value();
1096
1070
  reason = reason_str.c_str();
1097
1071
  }
1098
-
1072
+
1099
1073
  rats_error_t result = rats_reject_file_transfer(client_, transfer_id.c_str(), reason);
1100
1074
  return Napi::Boolean::New(env, result == RATS_SUCCESS);
1101
1075
  }
@@ -1271,7 +1245,22 @@ private:
1271
1245
  file_progress_callbacks[client_] = callback_data;
1272
1246
  rats_set_file_progress_callback(client_, file_progress_callback_wrapper, client_);
1273
1247
  }
1274
-
1248
+
1249
+ void OnFileRequest(const Napi::CallbackInfo& info) {
1250
+ Napi::Env env = info.Env();
1251
+
1252
+ if (info.Length() < 1 || !info[0].IsFunction()) {
1253
+ Napi::TypeError::New(env, "Expected callback function").ThrowAsJavaScriptException();
1254
+ return;
1255
+ }
1256
+
1257
+ auto callback_data = std::make_shared<CallbackData>(env);
1258
+ callback_data->callback = Napi::Persistent(info[0].As<Napi::Function>());
1259
+
1260
+ file_request_callbacks[client_] = callback_data;
1261
+ rats_set_file_request_callback(client_, file_request_callback_wrapper, client_);
1262
+ }
1263
+
1275
1264
  // Configuration persistence
1276
1265
  Napi::Value LoadConfiguration(const Napi::CallbackInfo& info) {
1277
1266
  Napi::Env env = info.Env();