chromium/chromeos/ash/components/data_migration/file_receiver.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/data_migration/file_receiver.h"

#include <utility>

#include "base/check.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "chromeos/ash/services/nearby/public/mojom/nearby_connections_types.mojom.h"

namespace data_migration {
namespace {

// Theoretically, this should never fail because the NC mojo service has told
// the caller that the transfer was a success by the time this function is
// called. That being said, file transfer is long and complex with lots of I/O
// involved. So this just double-checks the result before notifying the caller
// that the transfer succeeded.
bool DidFileTransferComplete(const base::FilePath& path,
                             int64_t expected_size_in_bytes) {
  static constexpr char kFailureLogPrefix[] =
      "File transfer verification failed. ";
  if (!base::PathExists(path)) {
    LOG(DFATAL) << kFailureLogPrefix << "File does not exist.";
    return false;
  }

  if (!base::PathIsReadable(path)) {
    LOG(DFATAL) << kFailureLogPrefix << "File is not even readable.";
    return false;
  }

  int64_t actual_file_size_in_bytes = 0;
  if (!base::GetFileSize(path, &actual_file_size_in_bytes)) {
    LOG(DFATAL) << kFailureLogPrefix << "Failed to get file size.";
    return false;
  }

  if (actual_file_size_in_bytes != expected_size_in_bytes) {
    LOG(DFATAL) << kFailureLogPrefix
                << "actual_file_size_in_bytes=" << actual_file_size_in_bytes
                << " expected_size_in_bytes=" << expected_size_in_bytes;
    return false;
  }
  return true;
}

}  // namespace

FileReceiver::Observer::Observer(
    base::OnceClosure on_file_registered_in,
    base::OnceCallback<void(bool)> on_file_transfer_complete_in)
    : on_file_registered(std::move(on_file_registered_in)),
      on_file_transfer_complete(std::move(on_file_transfer_complete_in)) {
  CHECK(on_file_registered);
  CHECK(on_file_transfer_complete);
}

FileReceiver::Observer::Observer(Observer&&) = default;

FileReceiver::Observer& FileReceiver::Observer::operator=(Observer&&) = default;

FileReceiver::Observer::~Observer() = default;

FileReceiver::FileReceiver(int64_t payload_id,
                           base::FilePath path,
                           Observer observer,
                           NearbyConnectionsManager* nearby_connections_manager)
    : payload_id_(payload_id),
      path_(std::move(path)),
      observer_(std::move(observer)),
      nearby_connections_manager_(nearby_connections_manager) {
  CHECK(!path_.empty());
  CHECK(nearby_connections_manager_);
  nearby_connections_manager_->RegisterPayloadStatusListener(payload_id_,
                                                             GetWeakPtr());
  RegisterPayloadPath(/*attempt_number=*/1);
}

FileReceiver::~FileReceiver() {
  if (!transfer_completed_successfully_) {
    VLOG(1) << "FileReceiver destroyed before file transfer completed. "
               "Canceling payload transfer.";
    // Invalidate weak ptrs first so that we do not synchronously get an
    // `OnStatusUpdate(<canceled>)` notification from our own call to
    // `NearbyConnectionsManager::Cancel()`.
    weak_ptr_factory_.InvalidateWeakPtrs();
    // Note this is a no-op if the transfer completed with a failure.
    nearby_connections_manager_->Cancel(payload_id_);
  }
  // Closes all file descriptors associated with this payload. Prevents them
  // from accumulating over the course of a long data migration.
  nearby_connections_manager_->ClearIncomingPayloadWithId(payload_id_);
}

void FileReceiver::RegisterPayloadPath(int attempt_number) {
  // Past 3 attempts, the partition is probably in such a bad state that
  // retrying more will not help.
  constexpr int kMaxNumAttempts = 3;
  if (attempt_number > kMaxNumAttempts) {
    LOG(ERROR) << "RegisterPayloadPath() failed " << kMaxNumAttempts
               << " times. File transfer is a failure";
    CompleteTransfer(/*verification_status=*/false);
    return;
  }

  // Tell the NC library that incoming payload from the remote device with
  // `payload_id_` should get written to `path_`. Registration must happen
  // before the incoming payload arrives, or file transmission will fail.
  nearby_connections_manager_->RegisterPayloadPath(
      payload_id_, path_,
      base::BindOnce(&FileReceiver::OnRegisterPayloadPathComplete,
                     file_receiver_weak_factory_.GetWeakPtr(), attempt_number));
}

void FileReceiver::OnRegisterPayloadPathComplete(
    int attempt_number,
    NearbyConnectionsManager::ConnectionsStatus result) {
  constexpr base::TimeDelta kRetryDelay = base::Milliseconds(250);
  if (result == NearbyConnectionsManager::ConnectionsStatus::kSuccess) {
    VLOG(1) << "data_migration file successfully registered with NC";
    CHECK(observer_.on_file_registered);
    std::move(observer_.on_file_registered).Run();
  } else {
    // This can legitimately happen from transient file I/O errors in the NC
    // library. There are no network operations involved though, so
    // exponential backoff and jitter are unnecessary.
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&FileReceiver::RegisterPayloadPath,
                       file_receiver_weak_factory_.GetWeakPtr(),
                       attempt_number + 1),
        kRetryDelay);
  }
}

void FileReceiver::OnStatusUpdate(PayloadTransferUpdatePtr update,
                                  std::optional<Medium> upgraded_medium) {
  CHECK(update);
  if (upgraded_medium) {
    VLOG(4) << "File transfer for using upgraded_medium " << *upgraded_medium;
  } else {
    VLOG(4) << "File transfer for has unknown upgraded_medium";
  }

  switch (update->status) {
    case ::nearby::connections::mojom::PayloadStatus::kSuccess:
      VLOG(1) << "File transfer completed";
      VerifyFileTransferResult(update->total_bytes);
      return;

    case ::nearby::connections::mojom::PayloadStatus::kInProgress: {
      // Catch for divide-by-zero when calculating `progress_as_percentage`.
      if (update->total_bytes == 0) {
        LOG(ERROR) << "File has expected size of zero bytes.";
        return;
      }
      if (update->bytes_transferred > update->total_bytes) {
        LOG(ERROR) << "File bytes_transferred(" << update->bytes_transferred
                   << ") > expected(" << update->total_bytes << ")";
        return;
      }
      VLOG(4) << "File transfer completion percentage: "
              << base::ClampFloor(
                     static_cast<double>(update->bytes_transferred) /
                     update->total_bytes * 100.f);
      return;
    }

    case ::nearby::connections::mojom::PayloadStatus::kCanceled:
    case ::nearby::connections::mojom::PayloadStatus::kFailure:
      LOG(ERROR) << "File transfer failed with status: " << update->status;
      CompleteTransfer(false);
      return;
  }
}

void FileReceiver::VerifyFileTransferResult(int64_t expected_size_in_bytes) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&DidFileTransferComplete, path_, expected_size_in_bytes),
      base::BindOnce(&FileReceiver::CompleteTransfer,
                     file_receiver_weak_factory_.GetWeakPtr()));
}

void FileReceiver::CompleteTransfer(bool verification_status) {
  if (!observer_.on_file_transfer_complete) {
    // This can only happen if the NC mojo service notifies us multiple times of
    // a successful or failed transfer.
    LOG(DFATAL) << "Received multiple payload completion status updates";
    return;
  }
  VLOG(1) << "File verification_status=" << verification_status;
  transfer_completed_successfully_ = verification_status;
  std::move(observer_.on_file_transfer_complete).Run(verification_status);
}

}  // namespace data_migration