LCOV - code coverage report
Current view: top level - src - data_transfer.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 79.4 % 616 489
Test Date: 2026-09-13 09:08:58 Functions: 87.9 % 66 58

            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
        

Generated by: LCOV version 2.0-1