Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "data_transfer.h"
19 :
20 : #include "base64.h"
21 : #include "fileutils.h"
22 : #include "manager.h"
23 : #include "client/jami_signal.h"
24 :
25 : #include <algorithm>
26 : #include <mutex>
27 : #include <cstdlib> // mkstemp
28 : #include <filesystem>
29 : #include <limits>
30 :
31 : #include <opendht/rng.h>
32 : #include <opendht/thread_pool.h>
33 :
34 : namespace jami {
35 :
36 : namespace {
37 :
38 : bool
39 19 : isExpectedFile(const std::filesystem::path& path, const std::string& sha3sum, std::size_t total)
40 : {
41 19 : std::error_code ec;
42 19 : return std::filesystem::file_size(path, ec) == total && fileutils::sha3File(path) == sha3sum;
43 : }
44 :
45 : bool
46 91 : isMissingPath(const std::filesystem::file_status& status, const std::error_code& ec)
47 : {
48 91 : return status.type() == std::filesystem::file_type::not_found || ec == std::errc::no_such_file_or_directory;
49 : }
50 :
51 : } // namespace
52 :
53 : libjami::DataTransferId
54 103 : generateUID(std::mt19937_64& engine)
55 : {
56 103 : return std::uniform_int_distribution<libjami::DataTransferId> {1, JAMI_ID_MAX_VAL}(engine);
57 : }
58 :
59 : namespace {
60 :
61 : constexpr size_t COMMIT_ID_SIZE = 40; // hex SHA-1
62 : constexpr size_t MAX_TID_SIZE = 20; // decimal uint64_t
63 :
64 : bool
65 256 : isLowerHex(std::string_view s)
66 : {
67 256 : return !s.empty() && std::all_of(s.begin(), s.end(), [](unsigned char c) {
68 10240 : return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
69 256 : });
70 : }
71 :
72 : bool
73 256 : isDecimal(std::string_view s)
74 : {
75 506 : return !s.empty() && s.size() <= MAX_TID_SIZE
76 4104 : && std::all_of(s.begin(), s.end(), [](unsigned char c) { return c >= '0' && c <= '9'; });
77 : }
78 :
79 : /** Characters that are path separators or otherwise special in file names on some platform */
80 : bool
81 108 : isSafeExtension(std::string_view ext)
82 : {
83 108 : if (ext.empty() || ext.size() > fileutils::MAX_EXTENSION_SIZE)
84 88 : return false;
85 20 : return std::none_of(ext.begin(), ext.end(), [](unsigned char c) {
86 60 : return c < 0x20 || c == 0x7f || c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"'
87 120 : || c == '<' || c == '>' || c == '|' || c == '.';
88 20 : });
89 : }
90 :
91 : } // namespace
92 :
93 : std::string
94 109 : getFileId(const std::string& commitId, const std::string& tid, const std::string& displayName)
95 : {
96 109 : if (commitId.size() != COMMIT_ID_SIZE || !isLowerHex(commitId) || !isDecimal(tid))
97 11 : return {};
98 98 : auto extension = fileutils::getFileExtension(displayName);
99 98 : if (!isSafeExtension(extension))
100 176 : return fmt::format("{}_{}", commitId, tid);
101 20 : return fmt::format("{}_{}.{}", commitId, tid, extension);
102 : }
103 :
104 : bool
105 155 : isValidFileId(std::string_view fileId) noexcept
106 : {
107 155 : auto sep = fileId.find('_');
108 155 : if (sep != COMMIT_ID_SIZE || !isLowerHex(fileId.substr(0, sep)))
109 6 : return false;
110 149 : auto rest = fileId.substr(sep + 1);
111 149 : auto dot = rest.find('.');
112 149 : if (dot == std::string_view::npos)
113 133 : return isDecimal(rest);
114 16 : return isDecimal(rest.substr(0, dot)) && isSafeExtension(rest.substr(dot + 1));
115 : }
116 :
117 156 : FileInfo::FileInfo(const std::shared_ptr<dhtnet::ChannelSocket>& channel,
118 : const std::string& fileId,
119 : const std::string& interactionId,
120 156 : const libjami::DataTransferInfo& info)
121 156 : : fileId_(fileId)
122 156 : , interactionId_(interactionId)
123 156 : , info_(info)
124 312 : , channel_(channel)
125 156 : {}
126 :
127 : void
128 180 : FileInfo::emit(libjami::DataTransferEventCode code)
129 : {
130 180 : if (finishedCb_ && code >= libjami::DataTransferEventCode::finished)
131 104 : finishedCb_(uint32_t(code));
132 180 : if (interactionId_ != "") {
133 : // Else it's an internal transfer
134 37 : runOnMainThread([info = info_, iid = interactionId_, fid = fileId_, code]() {
135 37 : emitSignal<libjami::DataTransferSignal::DataTransferEvent>(info.accountId,
136 37 : info.conversationId,
137 37 : iid,
138 37 : fid,
139 : uint32_t(code));
140 37 : });
141 : }
142 180 : }
143 :
144 80 : OutgoingFile::OutgoingFile(const std::shared_ptr<dhtnet::ChannelSocket>& channel,
145 : const std::string& fileId,
146 : const std::string& interactionId,
147 : const libjami::DataTransferInfo& info,
148 : size_t start,
149 80 : size_t end)
150 : : FileInfo(channel, fileId, interactionId, info)
151 80 : , start_(start)
152 80 : , end_(end)
153 : {
154 80 : std::filesystem::path fpath(info_.path);
155 80 : if (!std::filesystem::is_regular_file(fpath)) {
156 100 : dht::ThreadPool::io().run([channel = std::move(channel_)] { channel->shutdown(); });
157 50 : return;
158 : }
159 30 : stream_.open(fpath, std::ios::binary | std::ios::in);
160 30 : if (!stream_ || !stream_.is_open()) {
161 0 : dht::ThreadPool::io().run([channel = std::move(channel_)] { channel->shutdown(); });
162 0 : return;
163 : }
164 80 : }
165 :
166 80 : OutgoingFile::~OutgoingFile()
167 : {
168 80 : if (stream_ && stream_.is_open())
169 0 : stream_.close();
170 80 : if (channel_) {
171 60 : dht::ThreadPool::io().run([channel = std::move(channel_)] { channel->shutdown(); });
172 : }
173 80 : }
174 :
175 : void
176 80 : OutgoingFile::process()
177 : {
178 80 : if (!channel_ or !stream_ or !stream_.is_open())
179 50 : return;
180 30 : auto correct = false;
181 30 : stream_.seekg(static_cast<long>(start_), std::ios::beg);
182 : try {
183 30 : std::vector<char> buffer(UINT16_MAX, 0);
184 30 : std::error_code ec;
185 30 : auto pos = start_;
186 265 : while (!stream_.eof()) {
187 239 : stream_.read(buffer.data(),
188 239 : end_ > start_ ? static_cast<long>(std::min(end_ - pos, buffer.size()))
189 239 : : static_cast<long>(buffer.size()));
190 239 : auto gcount = stream_.gcount();
191 239 : pos += gcount;
192 239 : channel_->write(reinterpret_cast<const uint8_t*>(buffer.data()), gcount, ec);
193 239 : if (ec)
194 4 : break;
195 : }
196 30 : if (!ec)
197 26 : correct = true;
198 30 : stream_.close();
199 30 : } catch (const std::exception& e) {
200 0 : JAMI_WARNING("Failed to read from stream: {}", e.what());
201 0 : }
202 30 : if (!isUserCancelled_) {
203 : // NOTE: emit(code) MUST be changed to improve handling of multiple destinations
204 : // But for now, we can just avoid to emit errors to the client, because for outgoing
205 : // transfer in a swarm, for outgoingFiles, we know that the file is ok. And the peer
206 : // will retry the transfer if they need, so we don't need to show errors.
207 30 : if (!interactionId_.empty() && !correct)
208 2 : return;
209 28 : auto code = correct ? libjami::DataTransferEventCode::finished : libjami::DataTransferEventCode::closed_by_peer;
210 28 : emit(code);
211 : }
212 : }
213 :
214 : void
215 0 : OutgoingFile::cancel()
216 : {
217 : // Remove link, not original file
218 0 : auto path = fileutils::get_data_dir() / "conversation_data" / info_.accountId / info_.conversationId / fileId_;
219 0 : if (std::filesystem::is_symlink(path))
220 0 : dhtnet::fileutils::remove(path);
221 0 : isUserCancelled_ = true;
222 0 : emit(libjami::DataTransferEventCode::closed_by_host);
223 0 : }
224 :
225 76 : IncomingFile::IncomingFile(const std::shared_ptr<dhtnet::ChannelSocket>& channel,
226 : const libjami::DataTransferInfo& info,
227 : const std::string& fileId,
228 : const std::string& interactionId,
229 : const std::string& sha3Sum,
230 76 : const std::filesystem::path& temporaryPath)
231 : : FileInfo(channel, fileId, interactionId, info)
232 76 : , sha3Sum_(sha3Sum)
233 152 : , path_(temporaryPath)
234 : {
235 76 : stream_.open(path_, std::ios::binary | std::ios::out | std::ios::app);
236 76 : if (!stream_)
237 0 : return;
238 :
239 76 : emit(libjami::DataTransferEventCode::ongoing);
240 0 : }
241 :
242 76 : IncomingFile::~IncomingFile()
243 : {
244 : {
245 76 : std::lock_guard<std::mutex> lk(streamMtx_);
246 76 : if (stream_ && stream_.is_open())
247 0 : stream_.close();
248 76 : }
249 76 : if (channel_)
250 150 : dht::ThreadPool::io().run([channel = std::move(channel_)] { channel->shutdown(); });
251 76 : }
252 :
253 : void
254 1 : IncomingFile::cancel()
255 : {
256 1 : isUserCancelled_ = true;
257 : {
258 1 : std::lock_guard<std::mutex> lk(streamMtx_);
259 1 : if (stream_.is_open())
260 1 : stream_.close();
261 1 : }
262 1 : std::error_code ec;
263 1 : std::filesystem::remove(path_, ec);
264 1 : if (ec)
265 0 : JAMI_WARNING("Unable to remove canceled partial file {}: {}", path_, ec.message());
266 1 : emit(libjami::DataTransferEventCode::closed_by_peer);
267 1 : if (channel_)
268 2 : dht::ThreadPool::io().run([channel = std::move(channel_)] { channel->shutdown(); });
269 1 : }
270 :
271 : void
272 76 : IncomingFile::process()
273 : {
274 76 : if (!stream_.is_open()) {
275 0 : emit(libjami::DataTransferEventCode::invalid_pathname);
276 0 : if (channel_)
277 0 : dht::ThreadPool::io().run([channel = std::move(channel_)] { channel->shutdown(); });
278 0 : return;
279 : }
280 76 : channel_->setOnRecv([w = weak_from_this()](const uint8_t* buf, size_t len) {
281 223 : if (auto shared = w.lock()) {
282 223 : std::lock_guard<std::mutex> lk(shared->streamMtx_);
283 223 : if (!shared->stream_.is_open())
284 0 : return -1;
285 223 : shared->stream_.write(reinterpret_cast<const char*>(buf), static_cast<long>(len));
286 223 : if (!shared->stream_)
287 0 : return -1;
288 223 : shared->info_.bytesProgress = shared->stream_.tellp();
289 223 : return static_cast<int>(len);
290 446 : }
291 : // Data received after destruction
292 0 : JAMI_ERROR("{} bytes received after IncomingFile destruction.", len);
293 0 : return -1;
294 : });
295 76 : channel_->onShutdown([w = weak_from_this()](const std::error_code& /*error_code*/) {
296 76 : auto shared = w.lock();
297 76 : if (!shared)
298 1 : return;
299 : {
300 75 : std::lock_guard<std::mutex> lk(shared->streamMtx_);
301 75 : if (shared->stream_ && shared->stream_.is_open())
302 75 : shared->stream_.close();
303 75 : }
304 75 : auto correct = shared->sha3Sum_.empty();
305 75 : std::error_code ec;
306 75 : if (!correct) {
307 17 : if (shared->isUserCancelled_) {
308 0 : std::filesystem::remove(shared->path_, ec);
309 17 : } else if (shared->info_.bytesProgress < shared->info_.totalSize) {
310 1 : JAMI_WARNING("Channel for {} shut down before transfer was complete (progress: {}/{})",
311 : shared->info_.path,
312 : shared->info_.bytesProgress,
313 : shared->info_.totalSize);
314 16 : } else if (shared->info_.totalSize != 0 && shared->info_.bytesProgress > shared->info_.totalSize) {
315 0 : JAMI_WARNING("Removing {} larger than announced: {}/{}",
316 : shared->path_,
317 : shared->info_.bytesProgress,
318 : shared->info_.totalSize);
319 0 : std::filesystem::remove(shared->path_, ec);
320 : } else {
321 16 : auto sha3Sum = fileutils::sha3File(shared->path_);
322 16 : if (shared->sha3Sum_ == sha3Sum) {
323 14 : JAMI_LOG("New file received: {}", shared->info_.path);
324 14 : correct = true;
325 : } else {
326 2 : JAMI_WARNING(
327 : "Removing {} with expected size ({} bytes) but invalid sha3sum (expected: {}, actual: {})",
328 : shared->path_,
329 : shared->info_.totalSize,
330 : shared->sha3Sum_,
331 : sha3Sum);
332 2 : std::filesystem::remove(shared->path_, ec);
333 : }
334 16 : }
335 17 : if (ec) {
336 0 : JAMI_ERROR("Failed to remove file {}: {}", shared->path_, ec.message());
337 : }
338 : }
339 75 : auto installed = false;
340 75 : if (correct) {
341 72 : if (shared->installCb_) {
342 9 : installed = shared->installCb_(shared->path_);
343 : } else {
344 63 : std::filesystem::rename(shared->path_, shared->info_.path, ec);
345 63 : installed = !ec;
346 63 : if (ec)
347 0 : JAMI_ERROR("Failed to rename file from {} to {}: {}",
348 : shared->path_,
349 : shared->info_.path,
350 : ec.message());
351 : }
352 : }
353 75 : if (shared->isUserCancelled_)
354 0 : return;
355 147 : auto code = !correct ? libjami::DataTransferEventCode::closed_by_host
356 72 : : installed ? libjami::DataTransferEventCode::finished
357 : : libjami::DataTransferEventCode::invalid_pathname;
358 75 : shared->emit(code);
359 75 : dht::ThreadPool::io().run([s = std::move(shared)] {});
360 76 : });
361 : }
362 :
363 : //==============================================================================
364 :
365 : class TransferManager::Impl
366 : {
367 : public:
368 1276 : Impl(const std::string& accountId, const std::string& accountUri, const std::string& to, const std::mt19937_64& rand)
369 1276 : : accountId_(accountId)
370 1276 : , accountUri_(accountUri)
371 1276 : , to_(to)
372 1276 : , rand_(rand)
373 : {
374 1276 : if (!to_.empty()) {
375 493 : conversationDataPath_ = fileutils::get_data_dir() / accountId_ / "conversation_data" / to_;
376 493 : dhtnet::fileutils::check_dir(conversationDataPath_);
377 493 : waitingPath_ = conversationDataPath_ / "waiting";
378 : }
379 1276 : profilesPath_ = fileutils::get_data_dir() / accountId_ / "profiles";
380 1276 : accountProfilePath_ = fileutils::get_data_dir() / accountId / "profile.vcf";
381 1276 : loadWaiting();
382 1276 : }
383 :
384 1276 : ~Impl()
385 : {
386 1276 : std::lock_guard lk {mapMutex_};
387 1328 : for (auto& [channel, _] : outgoings_) {
388 104 : dht::ThreadPool::io().run([c = std::move(channel)] { c->shutdown(); });
389 : }
390 1276 : outgoings_.clear();
391 1276 : incomings_.clear();
392 1276 : vcards_.clear();
393 1276 : }
394 :
395 1276 : void loadWaiting()
396 : {
397 : try {
398 : // read file
399 2552 : auto file = fileutils::loadFile(waitingPath_);
400 : // load values
401 0 : msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
402 0 : std::lock_guard lk {mapMutex_};
403 0 : oh.get().convert(waitingIds_);
404 0 : auto changed = false;
405 0 : for (auto it = waitingIds_.begin(); it != waitingIds_.end();) {
406 0 : const auto& request = it->second;
407 0 : auto destination = std::filesystem::path(request.path);
408 0 : if (it->first != request.fileId || !isValidFileId(request.fileId)
409 0 : || (!destination.empty() && destination.is_relative())) {
410 0 : it = waitingIds_.erase(it);
411 0 : changed = true;
412 : } else {
413 0 : ++it;
414 : }
415 0 : }
416 0 : if (changed)
417 0 : saveWaiting();
418 1276 : } catch (const std::exception& e) {
419 1276 : return;
420 1276 : }
421 : }
422 25 : void saveWaiting()
423 : {
424 25 : std::ofstream file(waitingPath_, std::ios::trunc | std::ios::binary);
425 25 : msgpack::pack(file, waitingIds_);
426 25 : }
427 :
428 : std::string accountId_ {};
429 : std::string accountUri_ {};
430 : std::string to_ {};
431 : std::filesystem::path waitingPath_ {};
432 : std::filesystem::path profilesPath_ {};
433 : std::filesystem::path accountProfilePath_ {};
434 : std::filesystem::path conversationDataPath_ {};
435 :
436 : std::mutex mapMutex_ {};
437 : std::map<std::string, WaitingRequest> waitingIds_ {};
438 : std::map<std::shared_ptr<dhtnet::ChannelSocket>, std::shared_ptr<OutgoingFile>> outgoings_ {};
439 : std::map<std::string, std::shared_ptr<IncomingFile>> incomings_ {};
440 : std::map<std::pair<std::string, std::string>, std::shared_ptr<IncomingFile>> vcards_ {};
441 :
442 : std::mt19937_64 rand_;
443 : };
444 :
445 1276 : TransferManager::TransferManager(const std::string& accountId,
446 : const std::string& accountUri,
447 : const std::string& to,
448 1276 : const std::mt19937_64& rand)
449 1276 : : pimpl_ {std::make_unique<Impl>(accountId, accountUri, to, rand)}
450 1276 : {}
451 :
452 1276 : TransferManager::~TransferManager() {}
453 :
454 : void
455 82 : TransferManager::transferFile(const std::shared_ptr<dhtnet::ChannelSocket>& channel,
456 : const std::string& fileId,
457 : const std::string& interactionId,
458 : const std::string& path,
459 : size_t start,
460 : size_t end,
461 : OnFinishedCb onFinished)
462 : {
463 82 : std::lock_guard lk {pimpl_->mapMutex_};
464 82 : if (pimpl_->outgoings_.find(channel) != pimpl_->outgoings_.end())
465 2 : return;
466 80 : libjami::DataTransferInfo info;
467 80 : info.accountId = pimpl_->accountId_;
468 80 : info.conversationId = pimpl_->to_;
469 80 : info.path = path;
470 80 : auto f = std::make_shared<OutgoingFile>(channel, fileId, interactionId, info, start, end);
471 80 : f->onFinished([w = weak(), channel, onFinished = std::move(onFinished)](uint32_t code) {
472 28 : if (code == uint32_t(libjami::DataTransferEventCode::finished) && onFinished) {
473 5 : onFinished();
474 : }
475 : // schedule destroy outgoing transfer as not needed
476 28 : dht::ThreadPool().computation().run([w, channel] {
477 28 : if (auto sthis_ = w.lock()) {
478 28 : auto& pimpl = sthis_->pimpl_;
479 28 : std::lock_guard lk {pimpl->mapMutex_};
480 28 : auto itO = pimpl->outgoings_.find(channel);
481 28 : if (itO != pimpl->outgoings_.end())
482 28 : pimpl->outgoings_.erase(itO);
483 56 : }
484 28 : });
485 28 : });
486 80 : auto [outFile, _] = pimpl_->outgoings_.emplace(channel, std::move(f));
487 80 : dht::ThreadPool::io().run([w = std::weak_ptr<OutgoingFile>(outFile->second)] {
488 80 : if (auto of = w.lock())
489 80 : of->process();
490 80 : });
491 82 : }
492 :
493 : bool
494 1 : TransferManager::cancel(const std::string& fileId)
495 : {
496 1 : std::shared_ptr<IncomingFile> incoming;
497 1 : std::filesystem::path partialPath;
498 1 : auto canceled = false;
499 : {
500 1 : std::lock_guard lk {pimpl_->mapMutex_};
501 : // Remove from waiting, this avoid auto-download
502 1 : auto itW = pimpl_->waitingIds_.find(fileId);
503 1 : if (itW != pimpl_->waitingIds_.end()) {
504 1 : auto destination = std::filesystem::path(itW->second.path);
505 1 : if (destination.empty())
506 0 : partialPath = temporaryPath(fileId, path(fileId));
507 1 : else if (destination.is_absolute())
508 1 : partialPath = temporaryPath(fileId, destination);
509 1 : pimpl_->waitingIds_.erase(itW);
510 1 : JAMI_LOG("Cancel {}", fileId);
511 1 : pimpl_->saveWaiting();
512 1 : canceled = true;
513 1 : }
514 1 : auto itC = pimpl_->incomings_.find(fileId);
515 1 : if (itC != pimpl_->incomings_.end())
516 1 : incoming = itC->second;
517 1 : }
518 1 : if (incoming) {
519 1 : incoming->cancel();
520 1 : return true;
521 : }
522 0 : if (!partialPath.empty()) {
523 0 : std::error_code ec;
524 0 : std::filesystem::remove(partialPath, ec);
525 0 : if (ec)
526 0 : JAMI_WARNING("Unable to remove canceled partial file {}: {}", partialPath, ec.message());
527 : }
528 0 : return canceled;
529 1 : }
530 :
531 : bool
532 3 : TransferManager::info(const std::string& fileId, std::string& path, int64_t& total, int64_t& progress) const noexcept
533 : {
534 3 : std::unique_lock lk {pimpl_->mapMutex_};
535 3 : if (pimpl_->to_.empty())
536 0 : return false;
537 :
538 3 : auto itI = pimpl_->incomings_.find(fileId);
539 3 : auto itW = pimpl_->waitingIds_.find(fileId);
540 3 : std::filesystem::path transferPath;
541 : try {
542 3 : transferPath = this->path(fileId);
543 3 : path = transferPath.string();
544 0 : } catch (const std::filesystem::filesystem_error& e) {
545 0 : JAMI_WARNING("Unable to resolve transfer path for {}: {}", fileId, e.what());
546 0 : progress = 0;
547 0 : return false;
548 0 : }
549 3 : if (itI != pimpl_->incomings_.end()) {
550 0 : total = itI->second->info().totalSize;
551 0 : progress = itI->second->info().bytesProgress;
552 0 : return true;
553 : }
554 :
555 3 : std::error_code ec;
556 3 : if (std::filesystem::is_regular_file(transferPath, ec)) {
557 1 : auto fileSize = std::filesystem::file_size(transferPath, ec);
558 1 : if (ec) {
559 0 : JAMI_WARNING("Unable to read transfer file size for {}: {}", path, ec.message());
560 0 : progress = 0;
561 0 : return false;
562 : }
563 1 : progress = static_cast<int64_t>(
564 1 : std::min<uintmax_t>(fileSize, static_cast<uintmax_t>(std::numeric_limits<int64_t>::max())));
565 1 : if (itW != pimpl_->waitingIds_.end()) {
566 0 : total = static_cast<int64_t>(itW->second.totalSize);
567 : } else {
568 : // If not waiting it's finished
569 1 : total = progress;
570 : }
571 1 : return true;
572 : }
573 2 : if (ec && ec != std::errc::no_such_file_or_directory) {
574 1 : JAMI_WARNING("Unable to inspect transfer path {}: {}", path, ec.message());
575 : }
576 2 : if (itW != pimpl_->waitingIds_.end()) {
577 0 : total = static_cast<int64_t>(itW->second.totalSize);
578 0 : progress = 0;
579 0 : return true;
580 : }
581 : // Else we don't know infos there.
582 2 : progress = 0;
583 2 : return false;
584 3 : }
585 :
586 : bool
587 23 : TransferManager::indexFile(const std::string& fileId,
588 : const std::filesystem::path& candidate,
589 : const std::string& sha3sum,
590 : std::size_t total,
591 : bool independent)
592 : {
593 23 : return installIndex(fileId, candidate, sha3sum, total, independent, false);
594 : }
595 :
596 : bool
597 42 : TransferManager::installIndex(const std::string& fileId,
598 : const std::filesystem::path& candidate,
599 : const std::string& sha3sum,
600 : std::size_t total,
601 : bool independent,
602 : bool verifyCandidate)
603 : {
604 42 : if (!isValidFileId(fileId)) {
605 0 : JAMI_WARNING("Refusing to index file transfer with invalid id '{}'", fileId);
606 0 : return false;
607 : }
608 42 : auto canonicalPath = path(fileId);
609 : {
610 42 : std::lock_guard fileLock(dhtnet::fileutils::getFileLock(canonicalPath));
611 42 : std::error_code ec;
612 42 : auto canonicalStatus = std::filesystem::symlink_status(canonicalPath, ec);
613 42 : auto canonicalMissing = isMissingPath(canonicalStatus, ec);
614 42 : auto canonicalIsLink = canonicalStatus.type() == std::filesystem::file_type::symlink;
615 42 : auto canonicalIsValid = !canonicalMissing && !ec && isExpectedFile(canonicalPath, sha3sum, total);
616 42 : if (!canonicalIsValid || (independent && canonicalIsLink)) {
617 41 : if (candidate == canonicalPath || (verifyCandidate && !isExpectedFile(candidate, sha3sum, total)))
618 16 : return false;
619 : // Only a missing or stale link may be replaced; existing content is preserved.
620 25 : if (!canonicalMissing && (ec || !canonicalIsLink)) {
621 0 : JAMI_WARNING("Refusing to replace existing file transfer index {}", canonicalPath);
622 0 : return false;
623 : }
624 : libjami::DataTransferId stagingId;
625 : {
626 25 : std::lock_guard lk {pimpl_->mapMutex_};
627 25 : stagingId = generateUID(pimpl_->rand_);
628 25 : }
629 50 : auto stagedPath = canonicalPath.parent_path() / fmt::format(".jami-index-{}-{}.tmp", fileId, stagingId);
630 25 : auto stagedStatus = std::filesystem::symlink_status(stagedPath, ec);
631 25 : if (!isMissingPath(stagedStatus, ec)) {
632 0 : JAMI_WARNING("Refusing to replace occupied file transfer staging path {}", stagedPath);
633 0 : return false;
634 : }
635 25 : if (independent)
636 15 : std::filesystem::create_hard_link(candidate, stagedPath, ec);
637 : else
638 10 : std::filesystem::create_symlink(candidate, stagedPath, ec);
639 25 : if (ec) {
640 0 : JAMI_WARNING("Unable to link file transfer {} at {}, copying it: {}", fileId, stagedPath, ec.message());
641 0 : std::filesystem::copy_file(candidate, stagedPath, ec);
642 : // A link shares the verified content; only a copy can differ from it.
643 0 : if (!ec && !isExpectedFile(stagedPath, sha3sum, total))
644 0 : ec = std::make_error_code(std::errc::io_error);
645 0 : if (ec) {
646 0 : JAMI_ERROR("Unable to copy file transfer {} to {}: {}", fileId, stagedPath, ec.message());
647 0 : std::error_code removeError;
648 0 : std::filesystem::remove(stagedPath, removeError);
649 0 : return false;
650 : }
651 : }
652 25 : std::filesystem::rename(stagedPath, canonicalPath, ec);
653 25 : if (ec) {
654 0 : JAMI_ERROR("Unable to install file transfer index {} at {}: {}", fileId, canonicalPath, ec.message());
655 0 : std::error_code removeError;
656 0 : std::filesystem::remove(stagedPath, removeError);
657 0 : return false;
658 : }
659 25 : }
660 42 : }
661 :
662 26 : std::lock_guard lk {pimpl_->mapMutex_};
663 26 : if (pimpl_->waitingIds_.erase(fileId) != 0)
664 9 : pimpl_->saveWaiting();
665 26 : return true;
666 42 : }
667 :
668 : bool
669 3 : TransferManager::exportFile(const std::string& fileId,
670 : const std::filesystem::path& destination,
671 : const std::string& sha3sum,
672 : std::size_t total)
673 : {
674 3 : std::lock_guard fileLock(dhtnet::fileutils::getFileLock(destination));
675 3 : std::error_code ec;
676 3 : if (std::filesystem::equivalent(path(fileId), destination, ec) || isExpectedFile(destination, sha3sum, total))
677 2 : return true;
678 1 : auto status = std::filesystem::symlink_status(destination, ec);
679 1 : if (!isMissingPath(status, ec)) {
680 0 : JAMI_WARNING("Refusing to overwrite existing file {}", destination);
681 0 : return false;
682 : }
683 1 : auto source = std::filesystem::canonical(path(fileId), ec);
684 1 : if (ec) {
685 0 : JAMI_ERROR("Unable to resolve file transfer index {}: {}", fileId, ec.message());
686 0 : return false;
687 : }
688 1 : std::filesystem::create_hard_link(source, destination, ec);
689 1 : if (ec) {
690 0 : std::filesystem::copy_file(source, destination, ec);
691 0 : if (ec) {
692 0 : JAMI_ERROR("Unable to export file transfer {} to {}: {}", fileId, destination, ec.message());
693 0 : std::error_code removeError;
694 0 : std::filesystem::remove(destination, removeError);
695 0 : return false;
696 : }
697 : }
698 1 : return true;
699 3 : }
700 :
701 : bool
702 9 : TransferManager::installTransfer(const std::string& fileId,
703 : const std::filesystem::path& partial,
704 : const std::filesystem::path& destination,
705 : const std::string& sha3sum,
706 : std::size_t total)
707 : {
708 9 : const auto isIndex = destination.lexically_normal() == path(fileId).lexically_normal();
709 9 : std::lock_guard fileLock(dhtnet::fileutils::getFileLock(destination));
710 9 : std::error_code ec;
711 9 : auto status = std::filesystem::symlink_status(destination, ec);
712 9 : auto replaceable = isMissingPath(status, ec);
713 9 : if (!replaceable) {
714 0 : if (isExpectedFile(destination, sha3sum, total)) {
715 : // Redundant download: the destination already holds the file.
716 0 : std::filesystem::remove(partial, ec);
717 0 : return isIndex || indexFile(fileId, destination, sha3sum, total);
718 : }
719 : // Only a stale index link may be replaced; existing content is preserved.
720 0 : replaceable = isIndex && status.type() == std::filesystem::file_type::symlink;
721 : }
722 9 : if (!replaceable) {
723 0 : JAMI_WARNING("Refusing to overwrite existing file {}", destination);
724 0 : std::filesystem::remove(partial, ec);
725 0 : return false;
726 : }
727 9 : std::filesystem::rename(partial, destination, ec);
728 9 : if (ec) {
729 0 : JAMI_ERROR("Unable to install file transfer {} at {}: {}", fileId, destination, ec.message());
730 0 : std::filesystem::remove(partial, ec);
731 0 : return false;
732 : }
733 9 : if (isIndex || indexFile(fileId, destination, sha3sum, total))
734 9 : return true;
735 0 : std::filesystem::remove(destination, ec);
736 0 : return false;
737 9 : }
738 :
739 : TransferManager::WaitResult
740 19 : TransferManager::waitForTransfer(const std::string& fileId,
741 : const std::string& interactionId,
742 : const std::string& sha3sum,
743 : const std::string& path,
744 : std::size_t total)
745 : {
746 19 : if (!isValidFileId(fileId)) {
747 0 : JAMI_WARNING("Refusing to wait for file transfer with invalid id '{}'", fileId);
748 0 : return WaitResult::conflict;
749 : }
750 19 : if (!path.empty() && std::filesystem::path(path).is_relative())
751 0 : return WaitResult::conflict;
752 19 : auto canonicalPath = this->path(fileId);
753 19 : auto destination = path.empty() ? canonicalPath : std::filesystem::path(path);
754 19 : const auto isIndex = destination.lexically_normal() == canonicalPath.lexically_normal();
755 19 : if (installIndex(fileId, destination, sha3sum, total, false, true))
756 3 : return (isIndex || exportFile(fileId, destination, sha3sum, total)) ? WaitResult::complete
757 3 : : WaitResult::conflict;
758 16 : std::lock_guard lk(pimpl_->mapMutex_);
759 16 : auto itW = pimpl_->waitingIds_.find(fileId);
760 16 : if (itW != pimpl_->waitingIds_.end()) {
761 2 : auto waited = itW->second.path.empty() ? canonicalPath : std::filesystem::path(itW->second.path);
762 4 : auto matches = itW->second.interactionId == interactionId && itW->second.sha3sum == sha3sum
763 6 : && waited.lexically_normal() == destination.lexically_normal() && itW->second.totalSize == total;
764 2 : return matches ? WaitResult::waiting : WaitResult::conflict;
765 2 : }
766 14 : std::error_code ec;
767 14 : auto status = std::filesystem::symlink_status(destination, ec);
768 14 : if (!isMissingPath(status, ec) && !(isIndex && status.type() == std::filesystem::file_type::symlink)) {
769 0 : JAMI_WARNING("Refusing to overwrite existing file {}", destination);
770 0 : return WaitResult::conflict;
771 : }
772 : // A partial file left by a previous attempt is resumed by the transfer.
773 14 : pimpl_->waitingIds_[fileId] = {fileId, interactionId, sha3sum, path, total};
774 14 : pimpl_->saveWaiting();
775 14 : return WaitResult::waiting;
776 33 : }
777 :
778 : void
779 13 : TransferManager::onIncomingFileTransfer(const std::string& fileId,
780 : const std::shared_ptr<dhtnet::ChannelSocket>& channel,
781 : size_t start)
782 : {
783 13 : if (!isValidFileId(fileId)) {
784 0 : dht::ThreadPool().io().run([channel] { channel->shutdown(); });
785 0 : return;
786 : }
787 13 : std::unique_lock lk(pimpl_->mapMutex_);
788 : // Check if not already an incoming file for this id and that we are waiting this file
789 13 : auto itC = pimpl_->incomings_.find(fileId);
790 13 : if (itC != pimpl_->incomings_.end()) {
791 0 : dht::ThreadPool().io().run([channel] { channel->shutdown(); });
792 0 : return;
793 : }
794 13 : auto itW = pimpl_->waitingIds_.find(fileId);
795 13 : if (itW == pimpl_->waitingIds_.end()) {
796 0 : dht::ThreadPool().io().run([channel] { channel->shutdown(); });
797 0 : return;
798 : }
799 :
800 13 : libjami::DataTransferInfo info;
801 13 : info.accountId = pimpl_->accountId_;
802 13 : info.conversationId = pimpl_->to_;
803 13 : info.path = itW->second.path;
804 13 : info.totalSize = static_cast<int64_t>(itW->second.totalSize);
805 13 : info.bytesProgress = static_cast<int64_t>(start);
806 :
807 : // Receive into a private partial file next to the destination; the destination and the
808 : // index entry are only touched once the content has been verified.
809 13 : if (info.path.empty())
810 1 : info.path = path(fileId).string();
811 13 : const std::filesystem::path destinationPath(info.path);
812 13 : const auto expectedSize = itW->second.totalSize;
813 13 : const auto expectedSha3 = itW->second.sha3sum;
814 :
815 13 : auto ifile = std::make_shared<IncomingFile>(std::move(channel),
816 : info,
817 : fileId,
818 13 : itW->second.interactionId,
819 : expectedSha3,
820 26 : temporaryPath(fileId, destinationPath));
821 13 : auto res = pimpl_->incomings_.emplace(fileId, std::move(ifile));
822 13 : if (res.second) {
823 26 : res.first->second->onInstall(
824 26 : [w = weak(), fileId, destinationPath, expectedSha3, expectedSize](const std::filesystem::path& partial) {
825 9 : if (auto sthis = w.lock())
826 9 : return sthis->installTransfer(fileId, partial, destinationPath, expectedSha3, expectedSize);
827 0 : return false;
828 : });
829 13 : res.first->second->onFinished([w = weak(), fileId](uint32_t code) {
830 13 : if (auto sthis = w.lock()) {
831 13 : auto& pimpl = sthis->pimpl_;
832 13 : std::lock_guard lk {pimpl->mapMutex_};
833 13 : pimpl->incomings_.erase(fileId);
834 13 : if ((code == uint32_t(libjami::DataTransferEventCode::finished)
835 4 : || code == uint32_t(libjami::DataTransferEventCode::invalid_pathname))
836 17 : && pimpl->waitingIds_.erase(fileId) != 0) {
837 1 : pimpl->saveWaiting();
838 : }
839 26 : }
840 13 : });
841 13 : auto incoming = res.first->second;
842 13 : lk.unlock();
843 13 : incoming->process();
844 13 : }
845 13 : }
846 :
847 : std::filesystem::path
848 110 : TransferManager::path(const std::string& fileId) const
849 : {
850 110 : return pimpl_->conversationDataPath_ / fileId;
851 : }
852 :
853 : std::filesystem::path
854 32 : TransferManager::temporaryPath(const std::string& fileId, const std::filesystem::path& destination) const
855 : {
856 64 : return destination.parent_path() / fmt::format(".jami-{}-{}-{}.tmp", pimpl_->accountId_, pimpl_->to_, fileId);
857 : }
858 :
859 : void
860 67 : TransferManager::onIncomingProfile(const std::shared_ptr<dhtnet::ChannelSocket>& channel, const std::string& sha3Sum)
861 : {
862 67 : if (!channel)
863 0 : return;
864 :
865 67 : auto chName = channel->name();
866 67 : std::string_view name = chName;
867 67 : auto sep = name.find_last_of('?');
868 67 : if (sep != std::string::npos)
869 9 : name = name.substr(0, sep);
870 :
871 67 : auto lastSep = name.find_last_of('/');
872 67 : auto fileId = name.substr(lastSep + 1);
873 :
874 67 : auto deviceId = channel->deviceId().toString();
875 67 : auto cert = channel->peerCertificate();
876 67 : if (!cert || !cert->issuer || fileId.find(".vcf") == std::string::npos)
877 0 : return;
878 :
879 76 : auto uri = fileId == "profile.vcf" ? cert->issuer->getId().toString()
880 134 : : std::string(fileId.substr(0, fileId.size() - 4 /*.vcf*/));
881 :
882 67 : std::lock_guard lk(pimpl_->mapMutex_);
883 67 : auto idx = std::make_pair(deviceId, uri);
884 : // Check if not already an incoming file for this id and that we are waiting this file
885 67 : auto itV = pimpl_->vcards_.find(idx);
886 67 : if (itV != pimpl_->vcards_.end()) {
887 8 : dht::ThreadPool().io().run([channel] { channel->shutdown(); });
888 4 : return;
889 : }
890 :
891 63 : auto tid = generateUID(pimpl_->rand_);
892 63 : libjami::DataTransferInfo info;
893 63 : info.accountId = pimpl_->accountId_;
894 63 : info.conversationId = pimpl_->to_;
895 :
896 63 : auto recvDir = fileutils::get_cache_dir() / pimpl_->accountId_ / "vcard";
897 63 : dhtnet::fileutils::recursive_mkdir(recvDir);
898 126 : info.path = (recvDir / fmt::format("{:s}_{:s}_{}", deviceId, uri, tid)).string();
899 :
900 63 : auto ifile = std::make_shared<IncomingFile>(std::move(channel),
901 : info,
902 : "profile.vcf",
903 : "",
904 : sha3Sum,
905 126 : std::filesystem::path(info.path + ".tmp"));
906 63 : auto res = pimpl_->vcards_.emplace(idx, std::move(ifile));
907 63 : if (res.second) {
908 252 : res.first->second->onFinished([w = weak(),
909 63 : uri = std::move(uri),
910 63 : deviceId = std::move(deviceId),
911 63 : accountId = pimpl_->accountId_,
912 63 : cert = std::move(cert),
913 : path = info.path](uint32_t code) {
914 315 : dht::ThreadPool().computation().run([w,
915 63 : uri = std::move(uri),
916 63 : deviceId = std::move(deviceId),
917 63 : accountId = std::move(accountId),
918 63 : path = std::move(path),
919 : code] {
920 63 : if (auto sthis_ = w.lock()) {
921 63 : auto& pimpl = sthis_->pimpl_;
922 :
923 63 : auto destPath = sthis_->profilePath(uri);
924 : try {
925 : // Move profile to destination path
926 63 : std::lock_guard lock(dhtnet::fileutils::getFileLock(destPath));
927 63 : dhtnet::fileutils::recursive_mkdir(destPath.parent_path());
928 63 : std::filesystem::rename(path, destPath);
929 63 : if (!pimpl->accountUri_.empty() && uri == pimpl->accountUri_) {
930 : // If this is the account profile, link or copy it to the account profile path
931 1 : if (!fileutils::createFileLink(pimpl->accountProfilePath_, destPath)) {
932 0 : std::error_code ec;
933 0 : std::filesystem::copy_file(destPath, pimpl->accountProfilePath_, ec);
934 : }
935 : }
936 63 : } catch (const std::exception& e) {
937 0 : JAMI_ERROR("{}", e.what());
938 0 : }
939 :
940 63 : std::lock_guard lk {pimpl->mapMutex_};
941 63 : auto itO = pimpl->vcards_.find({deviceId, uri});
942 63 : if (itO != pimpl->vcards_.end())
943 63 : pimpl->vcards_.erase(itO);
944 63 : if (code == uint32_t(libjami::DataTransferEventCode::finished)) {
945 63 : emitSignal<libjami::ConfigurationSignal::ProfileReceived>(accountId, uri, destPath.string());
946 : }
947 126 : }
948 63 : });
949 63 : });
950 63 : res.first->second->process();
951 : }
952 87 : }
953 :
954 : std::filesystem::path
955 76 : TransferManager::profilePath(const std::string& contactId) const
956 : {
957 152 : return pimpl_->profilesPath_ / fmt::format("{}.vcf", base64::encode(contactId));
958 : }
959 :
960 : std::vector<WaitingRequest>
961 1817 : TransferManager::waitingRequests() const
962 : {
963 1817 : std::vector<WaitingRequest> res;
964 1817 : std::lock_guard lk(pimpl_->mapMutex_);
965 1817 : for (const auto& [fileId, req] : pimpl_->waitingIds_) {
966 1 : auto itC = pimpl_->incomings_.find(fileId);
967 1 : if (itC == pimpl_->incomings_.end())
968 1 : res.emplace_back(req);
969 : }
970 3632 : return res;
971 1816 : }
972 :
973 : bool
974 5 : TransferManager::isWaiting(const std::string& fileId) const
975 : {
976 5 : std::lock_guard lk(pimpl_->mapMutex_);
977 10 : return pimpl_->waitingIds_.find(fileId) != pimpl_->waitingIds_.end();
978 5 : }
979 :
980 : } // namespace jami
|