chromium/chrome/browser/ash/file_manager/trash_io_task.cc

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

#include "chrome/browser/ash/file_manager/trash_io_task.h"

#include <sys/xattr.h>

#include "ash/metrics/histogram_macros.h"
#include "base/containers/adapters.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/i18n/time_formatting.h"
#include "base/ranges/algorithm.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/system/sys_info.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/io_task_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/trash_common_util.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "google_apis/common/task_util.h"

namespace file_manager::io_task {
namespace {

// Generates and updates the `entry` with the standard contents of the
// individual .trashinfo files which contains the files original path (to
// restore to) and the deletion date.
bool UpdateTrashInfoContents(const base::FilePath& original_path,
                             const base::FilePath& trash_parent_path,
                             const base::FilePath& prefix_restore_path,
                             TrashEntry& entry) {
  std::string relative_restore_path = original_path.value();
  if (!file_manager::util::ReplacePrefix(
          &relative_restore_path,
          trash_parent_path.AsEndingWithSeparator().value(), "")) {
    return false;
  }

  base::FilePath prefix = (prefix_restore_path.IsAbsolute())
                              ? prefix_restore_path
                              : base::FilePath("/").Append(prefix_restore_path);

  entry.trash_info_contents =
      base::StrCat({"[Trash Info]\nPath=",
                    base::EscapePath(prefix.AsEndingWithSeparator().value()),
                    base::EscapePath(relative_restore_path), "\nDeletionDate=",
                    base::TimeFormatAsIso8601(entry.deletion_time), "\n"});
  return true;
}

storage::FileSystemOperationRunner::OperationID StartCreateDirectoryOnIOThread(
    scoped_refptr<storage::FileSystemContext> file_system_context,
    const storage::FileSystemURL url,
    storage::FileSystemOperationRunner::StatusCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  return file_system_context->operation_runner()->CreateDirectory(
      url, /*exclusive=*/false, /*recursive=*/true, std::move(callback));
}

bool WriteMetadataFileOnBlockingThread(const base::FilePath& destination_path,
                                       const std::string& contents) {
  // If the metadata file already exists either a previous copy failed or
  // the file has been tampered with to overwrite. Try to delete the file before
  // proceeding. `DeleteFile` will succeed if the file does not exist.
  if (!base::DeleteFile(destination_path)) {
    PLOG(ERROR) << "Failed to remove existing metadata file";
    return false;
  }
  return base::WriteFile(destination_path, contents);
}

bool SetTrashDirectoryPermissions(const base::FilePath& trash_directory) {
  return base::SetPosixFilePermissions(
      trash_directory, base::FILE_PERMISSION_READ_BY_USER |
                           base::FILE_PERMISSION_WRITE_BY_USER |
                           base::FILE_PERMISSION_EXECUTE_BY_USER |
                           base::FILE_PERMISSION_EXECUTE_BY_GROUP |
                           base::FILE_PERMISSION_EXECUTE_BY_OTHERS);
}

void RecordDirectorySetupMetric(trash::DirectorySetupUmaType type) {
  UMA_HISTOGRAM_ENUMERATION(trash::kDirectorySetupHistogramName, type);
}

void RecordFailedTrashingMetric(trash::FailedTrashingUmaType type) {
  UMA_HISTOGRAM_ENUMERATION(trash::kFailedTrashingHistogramName, type);
}

base::File::Error SetTrackedExtendedAttribute(const base::FilePath& path) {
  auto tracked_name = base::StrCat({"trash_", path.BaseName().value()});
  if (lsetxattr(path.value().c_str(), trash::kTrackedDirectoryName,
                tracked_name.c_str(), tracked_name.size(), 0) < 0) {
    RecordDirectorySetupMetric(trash::DirectorySetupUmaType::FAILED_XATTR);
    PLOG(WARNING) << "Failed to set the xattr " << trash::kTrackedDirectoryName
                  << "=" << tracked_name << " on " << path;
  }
  return base::File::FILE_OK;
}

TrashEntry::TrashEntry() : deletion_time(base::Time::Now()) {}
TrashEntry::~TrashEntry() = default;

TrashEntry::TrashEntry(TrashEntry&& other) = default;
TrashEntry& TrashEntry::operator=(TrashEntry&& other) = default;

}  // namespace

TrashIOTask::TrashIOTask(
    std::vector<storage::FileSystemURL> file_urls,
    Profile* profile,
    scoped_refptr<storage::FileSystemContext> file_system_context,
    const base::FilePath base_path,
    bool show_notification)
    : IOTask(show_notification),
      profile_(profile),
      file_system_context_(file_system_context),
      base_path_(base_path) {
  progress_.state = State::kQueued;
  progress_.type = OperationType::kTrash;
  progress_.bytes_transferred = 0;
  progress_.total_bytes = 0;

  for (const auto& url : file_urls) {
    progress_.sources.emplace_back(url, std::nullopt);
    trash_entries_.emplace_back();
  }
}

TrashIOTask::~TrashIOTask() {
  if (operation_id_) {
    content::GetIOThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            [](scoped_refptr<storage::FileSystemContext> file_system_context,
               storage::FileSystemOperationRunner::OperationID operation_id) {
              file_system_context->operation_runner()->Cancel(
                  operation_id, base::DoNothing());
            },
            file_system_context_, *operation_id_));
  }
}

void TrashIOTask::Execute(IOTask::ProgressCallback progress_callback,
                          IOTask::CompleteCallback complete_callback) {
  progress_callback_ = std::move(progress_callback);
  complete_callback_ = std::move(complete_callback);

  if (progress_.sources.size() == 0) {
    Complete(State::kSuccess);
    return;
  }

  // Build the list of known paths that are enabled, for now Downloads is a bind
  // mount at MyFiles/Downloads so treat them as separate volumes.
  free_space_map_ =
      trash::GenerateEnabledTrashLocationsForProfile(profile_, base_path_);
  progress_.state = State::kInProgress;

  UpdateTrashEntry(0);
}

// Calls the completion callback for the task. `progress_` should not be
// accessed after calling this.
void TrashIOTask::Complete(State state) {
  progress_.state = state;
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(complete_callback_), std::move(progress_)));
}

void TrashIOTask::UpdateTrashEntry(size_t source_idx) {
  base::FilePath source_path = progress_.sources[source_idx].url.path();

  if (!base_path_.empty() && !source_path.IsAbsolute()) {
    source_path = base_path_.Append(source_path);
  }

  // Use a std::map::reverse_iterator because insertions into a std::map are
  // sorted by key. base::FilePath keys will insert in lexicographical order
  // however in the case of nested directories, reverse lexicographical order is
  // preferred to ensure the closer parent path by depth is chosen.
  const trash::TrashPathsMap::reverse_iterator& trash_parent_path_it =
      base::ranges::find_if(base::Reversed(free_space_map_),
                            [&source_path](const auto& it) {
                              return it.first.IsParent(source_path);
                            });

  if (trash_parent_path_it == free_space_map_.rend()) {
    // The `source_path` is not parented at a supported Trash location, bail
    // out completely.
    progress_.sources[source_idx].error =
        base::File::FILE_ERROR_INVALID_OPERATION;
    Complete(State::kError);
    return;
  }

  trash::TrashLocation& trash_location = trash_parent_path_it->second;
  const base::FilePath trash_parent_path = trash_parent_path_it->first;
  TrashEntry& entry = trash_entries_[source_idx];
  entry.trash_mount_path = trash_parent_path;
  entry.relative_trash_path = trash_location.relative_folder_path;

  if (!UpdateTrashInfoContents(source_path, trash_parent_path,
                               trash_location.prefix_restore_path, entry)) {
    // If we can't update the trash entry, update the source error and finish
    // with an error.
    progress_.sources[source_idx].error =
        base::File::FILE_ERROR_INVALID_OPERATION;
    Complete(State::kError);
    return;
  }

  if (!trash_location.require_setup) {
    GetFreeDiskSpace(source_idx, trash_parent_path_it);
    return;
  }

  ValidateAndDecrementFreeSpace(source_idx, trash_parent_path_it);
}

void TrashIOTask::ValidateAndDecrementFreeSpace(
    size_t source_idx,
    const trash::TrashPathsMap::reverse_iterator& it) {
  int trash_contents_size =
      trash_entries_[source_idx].trash_info_contents.size();
  progress_.total_bytes += trash_contents_size;

  if (trash_contents_size > it->second.free_space) {
    // TODO(b/231830211): We probably don't have to bail out here, we can check
    // if an error is set on `progress_.sources` before trashing. This will
    // enable trashes with mixed sources (some no space, some with space) to
    // finish.
    progress_.sources[source_idx].error = base::File::FILE_ERROR_NO_SPACE;
    Complete(State::kError);
    return;
  }

  it->second.free_space -= trash_contents_size;
  GetFileSize(source_idx);
}

// Computes the total size of all source files and stores it in
// `progress_.total_bytes`.
void TrashIOTask::GetFileSize(size_t source_idx) {
  DCHECK(source_idx < progress_.sources.size());
  content::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          &GetFileMetadataOnIOThread, file_system_context_,
          progress_.sources[source_idx].url,
          storage::FileSystemOperation::GetMetadataFieldSet(
              {storage::FileSystemOperation::GetMetadataField::kSize,
               storage::FileSystemOperation::GetMetadataField::kRecursiveSize}),
          google_apis::CreateRelayCallback(
              base::BindOnce(&TrashIOTask::GotFileSize,
                             weak_ptr_factory_.GetWeakPtr(), source_idx))));
}

// Helper function to GetFileSize() that is called when the metadata for a file
// is retrieved.
void TrashIOTask::GotFileSize(size_t source_idx,
                              base::File::Error error,
                              const base::File::Info& file_info) {
  DCHECK(source_idx < progress_.sources.size());
  if (error != base::File::FILE_OK) {
    progress_.sources[source_idx].error = error;
    Complete(State::kError);
    return;
  }

  progress_.total_bytes += file_info.size;
  trash_entries_[source_idx].source_file_size = file_info.size;

  if (source_idx < progress_.sources.size() - 1) {
    UpdateTrashEntry(source_idx + 1);
    return;
  }

  auto it = free_space_map_.cbegin();
  SetupSubDirectory(it, it->second.trash_files);
}

void TrashIOTask::GetFreeDiskSpace(
    size_t source_idx,
    const trash::TrashPathsMap::reverse_iterator& it) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace,
                     it->second.mount_point_path),
      base::BindOnce(&TrashIOTask::GotFreeDiskSpace,
                     weak_ptr_factory_.GetWeakPtr(), source_idx,
                     base::OwnedRef(it)));
}

base::FilePath TrashIOTask::MakeRelativeFromBasePath(
    const base::FilePath& absolute_path) {
  if (base_path_.empty() || !base_path_.IsParent(absolute_path)) {
    return absolute_path;
  }
  std::string relative_path = absolute_path.value();
  if (!file_manager::util::ReplacePrefix(
          &relative_path, base_path_.AsEndingWithSeparator().value(), "")) {
    LOG(ERROR) << "Failed to make absolute path relative";
    return absolute_path;
  }
  return base::FilePath(relative_path);
}

base::FilePath TrashIOTask::MakeRelativePathAbsoluteFromBasePath(
    const base::FilePath& relative_path) {
  if (base_path_.empty() || base_path_.IsParent(relative_path) ||
      relative_path.IsAbsolute()) {
    return relative_path;
  }
  return base_path_.Append(relative_path);
}

void TrashIOTask::GotFreeDiskSpace(
    size_t source_idx,
    const trash::TrashPathsMap::reverse_iterator& it,
    int64_t free_space) {
  trash::TrashLocation& trash_location = it->second;
  const base::FilePath& trash_parent_path = it->first;
  base::FilePath trash_path = MakeRelativeFromBasePath(
      trash_parent_path.Append(trash_location.relative_folder_path));
  trash_location.trash_files =
      CreateFileSystemURL(progress_.sources[source_idx].url,
                          trash_path.Append(trash::kFilesFolderName));
  trash_location.trash_info =
      CreateFileSystemURL(progress_.sources[source_idx].url,
                          trash_path.Append(trash::kInfoFolderName));
  trash_location.free_space = free_space;
  trash_location.require_setup = true;

  ValidateAndDecrementFreeSpace(source_idx, it);
}

void TrashIOTask::SetupSubDirectory(
    trash::TrashPathsMap::const_iterator& it,
    const storage::FileSystemURL trash_subdirectory) {
  // All enabled trash directories exist in the `free_space_map_` however some
  // may not be used for this IO task. Skip the ones that don't require setup.
  if (!it->second.require_setup) {
    it++;
    if (it == free_space_map_.end()) {
      GenerateDestinationURL(/*source_idx=*/0, /*output_idx=*/0);
      return;
    }
    SetupSubDirectory(it, it->second.trash_files);
    return;
  }

  auto on_setup_complete_callback = base::BindOnce(
      &TrashIOTask::OnSetupSubDirectory, weak_ptr_factory_.GetWeakPtr(),
      base::OwnedRef(it), trash_subdirectory);

  content::GetIOThreadTaskRunner({})->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&StartCreateDirectoryOnIOThread, file_system_context_,
                     trash_subdirectory,
                     base::BindPostTaskToCurrentDefault(
                         base::BindOnce(&TrashIOTask::SetDirectoryTracking,
                                        weak_ptr_factory_.GetWeakPtr(),
                                        std::move(on_setup_complete_callback),
                                        MakeRelativePathAbsoluteFromBasePath(
                                            trash_subdirectory.path())),
                         FROM_HERE)),
      base::BindOnce(&TrashIOTask::SetCurrentOperationID,
                     weak_ptr_factory_.GetWeakPtr()));
}

void TrashIOTask::SetDirectoryTracking(
    base::OnceCallback<void(base::File::Error)> on_setup_complete_callback,
    const base::FilePath& trash_subdirectory,
    base::File::Error error) {
  if (error != base::File::FILE_OK) {
    std::move(on_setup_complete_callback).Run(error);
    return;
  }

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&SetTrackedExtendedAttribute,
                     std::move(trash_subdirectory)),
      std::move(on_setup_complete_callback));
}

void TrashIOTask::OnSetupSubDirectory(
    trash::TrashPathsMap::const_iterator& it,
    const storage::FileSystemURL trash_subdirectory,
    base::File::Error error) {
  if (error != base::File::FILE_OK) {
    auto failed_directory_uma_type =
        (trash_subdirectory == it->second.trash_files)
            ? trash::DirectorySetupUmaType::FAILED_FILES_FOLDER
            : trash::DirectorySetupUmaType::FAILED_INFO_FOLDER;
    RecordDirectorySetupMetric(failed_directory_uma_type);
    LOG(ERROR) << "Failed setting up a trash subfolder: "
               << static_cast<int>(failed_directory_uma_type);
    // TODO(b/231830211): We can potentially continue if one .Trash directory
    // fails to create, but we should also rollback if the files directory
    // succeeds but info fails.
    Complete(State::kError);
    return;
  }

  // Make sure to setup the .Trash/info directory after the .Trash/files
  // directory.
  if (trash_subdirectory == it->second.trash_files) {
    SetupSubDirectory(it, it->second.trash_info);
    return;
  }

  // We have to ensure the permission bits are appropriately setup to allow
  // system daemons access to traverse the folder. By default the permissions
  // are setup as 0700 when they should be 0711.
  auto absolute_trash_path =
      MakeRelativePathAbsoluteFromBasePath(trash_subdirectory.path().DirName());
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&SetTrashDirectoryPermissions,
                     std::move(absolute_trash_path)),
      base::BindOnce(&TrashIOTask::OnSetDirectoryPermissions,
                     weak_ptr_factory_.GetWeakPtr(), base::OwnedRef(it)));
}

void TrashIOTask::OnSetDirectoryPermissions(
    trash::TrashPathsMap::const_iterator& it,
    bool set_permissions_success) {
  if (!set_permissions_success) {
    RecordDirectorySetupMetric(
        trash::DirectorySetupUmaType::FAILED_PARENT_FOLDER_PERMISSIONS);
    LOG(ERROR) << "Failed setting directory permissions";
    Complete(State::kError);
    return;
  }

  it++;
  // If we've have no more trash directory to setup, start trashing files.
  if (it == free_space_map_.end()) {
    GenerateDestinationURL(/*source_idx=*/0, /*output_idx=*/0);
    return;
  }

  SetupSubDirectory(it, it->second.trash_files);
}

void TrashIOTask::GenerateDestinationURL(size_t source_idx, size_t output_idx) {
  DCHECK(source_idx < progress_.sources.size());
  DCHECK(source_idx < trash_entries_.size());

  const TrashEntry& entry = trash_entries_[source_idx];
  const auto trash_path = MakeRelativeFromBasePath(
      entry.trash_mount_path.Append(entry.relative_trash_path)
          .Append(trash::kFilesFolderName));

  const storage::FileSystemURL files_location =
      CreateFileSystemURL(progress_.sources[source_idx].url, trash_path);
  util::GenerateUnusedFilename(
      files_location, progress_.sources[source_idx].url.path().BaseName(),
      file_system_context_,
      base::BindOnce(&TrashIOTask::WriteMetadata,
                     weak_ptr_factory_.GetWeakPtr(), source_idx, output_idx,
                     files_location));
}

void TrashIOTask::WriteMetadata(
    size_t source_idx,
    size_t output_idx,
    const storage::FileSystemURL& files_folder_location,
    base::FileErrorOr<storage::FileSystemURL> destination_result) {
  if (!destination_result.has_value()) {
    progress_.outputs.emplace_back(files_folder_location, std::nullopt);
    TrashComplete(source_idx, output_idx, destination_result.error());
    return;
  }
  const base::FilePath absolute_trash_path =
      trash_entries_[source_idx].trash_mount_path.Append(
          trash_entries_[source_idx].relative_trash_path);
  const std::string file_name =
      destination_result.value().path().BaseName().value();

  const base::FilePath destination_path = trash::GenerateTrashPath(
      absolute_trash_path, trash::kInfoFolderName, file_name);
  progress_.outputs.emplace_back(
      CreateFileSystemURL(progress_.sources[source_idx].url, destination_path),
      std::nullopt);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&WriteMetadataFileOnBlockingThread, destination_path,
                     trash_entries_[source_idx].trash_info_contents),
      base::BindOnce(&TrashIOTask::OnWriteMetadata,
                     weak_ptr_factory_.GetWeakPtr(), source_idx, output_idx,
                     destination_result.value()));
}

void TrashIOTask::OnWriteMetadata(size_t source_idx,
                                  size_t output_idx,
                                  const storage::FileSystemURL& destination_url,
                                  bool success) {
  if (!success) {
    RecordFailedTrashingMetric(
        trash::FailedTrashingUmaType::FAILED_WRITING_METADATA);
    TrashComplete(source_idx, output_idx, base::File::FILE_ERROR_FAILED);
    return;
  }

  last_metadata_url_ = progress_.outputs[output_idx].url;
  progress_.outputs[output_idx].error = base::File::FILE_OK;
  TrashFile(source_idx, output_idx, destination_url);
}

void TrashIOTask::TrashFile(size_t source_idx,
                            size_t output_idx,
                            const storage::FileSystemURL& destination_url) {
  DCHECK(source_idx < progress_.sources.size());
  DCHECK(output_idx < progress_.outputs.size());
  progress_.outputs.emplace_back(destination_url, std::nullopt);

  last_progress_size_ = 0;

  const storage::FileSystemURL& source_url = progress_.sources[source_idx].url;

  // File browsers generally default to preserving mtimes on copy/move so we
  // should do the same.
  storage::FileSystemOperation::CopyOrMoveOptionSet options = {
      storage::FileSystemOperation::CopyOrMoveOption::kPreserveLastModified};

  auto complete_callback = base::BindPostTaskToCurrentDefault(base::BindOnce(
      &TrashIOTask::OnMoveComplete, weak_ptr_factory_.GetWeakPtr(), source_idx,
      output_idx + 1));

  // For move operations that occur on the same file system, the progress
  // callback is never invoked.
  content::GetIOThreadTaskRunner({})->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&StartMoveFileLocalOnIOThread, file_system_context_,
                     source_url, destination_url, options,
                     std::move(complete_callback)),
      base::BindOnce(&TrashIOTask::SetCurrentOperationID,
                     weak_ptr_factory_.GetWeakPtr()));
}

void TrashIOTask::OnMoveComplete(size_t source_idx,
                                 size_t output_idx,
                                 base::File::Error error) {
  DCHECK(source_idx < progress_.sources.size());
  DCHECK(output_idx < progress_.outputs.size());
  if (error != base::File::FILE_OK) {
    LOG(ERROR) << "Failed to move the file to trash folder: " << error;
    RecordFailedTrashingMetric(
        trash::FailedTrashingUmaType::FAILED_MOVING_FILE);
    auto complete_callback = base::BindPostTaskToCurrentDefault(
        base::BindOnce(&TrashIOTask::TrashComplete,
                       weak_ptr_factory_.GetWeakPtr(), source_idx, output_idx));

    content::GetIOThreadTaskRunner({})->PostTaskAndReplyWithResult(
        FROM_HERE,
        base::BindOnce(&StartDeleteOnIOThread, file_system_context_,
                       last_metadata_url_, std::move(complete_callback)),
        base::BindOnce(&TrashIOTask::SetCurrentOperationID,
                       weak_ptr_factory_.GetWeakPtr()));
    return;
  }

  TrashComplete(source_idx, output_idx, error);
}

void TrashIOTask::TrashComplete(size_t source_idx,
                                size_t output_idx,
                                base::File::Error error) {
  DCHECK(source_idx < progress_.sources.size());
  DCHECK(output_idx < progress_.outputs.size());
  operation_id_.reset();
  progress_.sources[source_idx].error = error;
  progress_.outputs[output_idx].error = error;
  progress_.bytes_transferred +=
      trash_entries_[source_idx].trash_info_contents.size() +
      (trash_entries_[source_idx].source_file_size - last_progress_size_);

  if (source_idx < progress_.sources.size() - 1) {
    progress_callback_.Run(progress_);
    GenerateDestinationURL(source_idx + 1, output_idx + 1);
  } else {
    for (const auto& source : progress_.sources) {
      if (source.error != base::File::FILE_OK) {
        Complete(State::kError);
        return;
      }
    }
    Complete(State::kSuccess);
  }
}

const storage::FileSystemURL TrashIOTask::CreateFileSystemURL(
    const storage::FileSystemURL& original_url,
    const base::FilePath& path) {
  return file_system_context_->CreateCrackedFileSystemURL(
      original_url.storage_key(), original_url.type(), path);
}

void TrashIOTask::Cancel() {
  progress_.state = State::kCancelled;
  // Any inflight operation will be cancelled when the task is destroyed.
}

void TrashIOTask::SetCurrentOperationID(
    storage::FileSystemOperationRunner::OperationID id) {
  operation_id_.emplace(id);
}

}  // namespace file_manager::io_task