/*
 * Copyright 2019 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <buildboxcasd_localcasinstance.h>
#include <buildboxcasd_metricnames.h>
#include <buildboxcasd_server.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_filestager.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_identity.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_merklize.h>
#include <buildboxcommon_systemutils.h>
#include <buildboxcommon_timeutils.h>
#include <buildboxcommonmetrics_countingmetricutil.h>
#include <buildboxcommonmetrics_countingmetricvalue.h>
#include <buildboxcommonmetrics_durationmetrictimer.h>
#include <buildboxcommonmetrics_metricguard.h>

#include <algorithm>
#include <cstdint>
#include <exception>
#include <fcntl.h>
#include <grpcpp/support/status.h>
#include <memory>
#include <optional>
#include <utility>
#ifdef __linux__
#include <sys/xattr.h>
#endif

using namespace buildboxcasd;
using namespace buildboxcommon;

/*
 *   Helper method that stores err in status, and logs the message.
 */
void logAndStoreMessage(const grpc::StatusCode &code, const std::string &err,
                        google::rpc::Status *status)
{
    status->set_message(err);
    status->set_code(code);
    BUILDBOX_LOG_ERROR(err);
}

LocalCasInstance::LocalCasInstance(
    const std::shared_ptr<LocalCas> &storage,
    const std::unordered_map<buildboxcommon::StageTreeRequest::StagingMode,
                             std::shared_ptr<buildboxcommon::FileStager>>
        &fileStagers,
    const std::shared_ptr<ThreadPool> &digestThreadPool,
    const std::shared_ptr<std::unordered_set<std::string>>
        &allowedPreUnstageCommands,
    const std::string &instance_name)
    : CasInstance(instance_name), d_storage(storage),
      d_fileStagers(fileStagers), d_digestThreadPool(digestThreadPool),
      d_allowedPreUnstageCommands(allowedPreUnstageCommands), d_uid(geteuid())
{
}

LocalCasInstance::LocalCasInstance(const LocalCasInstance &other,
                                   const std::string &instanceName,
                                   const std::string &root)
    : CasInstance(other, instanceName, root), d_storage(other.d_storage),
      d_fileStagers(other.d_fileStagers),
      d_digestThreadPool(other.d_digestThreadPool), d_uid(other.d_uid)
{
}

std::shared_ptr<CasInstance>
LocalCasInstance::clone(const std::string &instanceName,
                        const std::string &root)
{
    return std::make_shared<LocalCasInstance>(*this, instanceName, root);
}

grpc::Status LocalCasInstance::GetLocalServerDetails(
    const GetLocalServerDetailsRequest &request, LocalServerDetails *response)
{
    if (chrooted()) {
        // The storage root would anyway not be accessible in the sandbox.
        return Status(
            grpc::StatusCode::PERMISSION_DENIED,
            "Local server details not available in sandboxed instance");
    }

    const auto &identity = getApplicationIdentity();
    response->set_hostname(identity.d_hostname);
    response->set_process_uid(identity.d_process_uid);
    response->set_storage_root(d_storage->path());

    return grpc::Status::OK;
}

grpc::Status
LocalCasInstance::FindMissingBlobs(const FindMissingBlobsRequest &request,
                                   FindMissingBlobsResponse *response)
{
    // Returns a list of digests that are *not* in the CAS.
    BUILDBOX_LOG_INFO(
        "LocalCasInstance::FindMissingBlobs request for instance name \""
        << request.instance_name() << "\" for "
        << request.blob_digests().size() << " digest(s)");

    for (const Digest &digest : request.blob_digests()) {
        bool blob_in_cas = false;

        try {
            blob_in_cas = d_storage->hasBlob(digest);
        }
        catch (const std::runtime_error &) {
            BUILDBOX_LOG_ERROR("Could not determine if "
                               << digest.hash() << "is in local CAS.");
        }

        if (!blob_in_cas) {
            Digest *entry = response->add_missing_blob_digests();
            entry->CopyFrom(digest);
        }
    }

    buildboxcommon::buildboxcommonmetrics::CountingMetricUtil::
        recordCounterMetric(MetricNames::COUNTER_NUM_BLOBS_FIND_MISSING,
                            request.blob_digests().size());

    return grpc::Status::OK;
}

grpc::Status
LocalCasInstance::BatchUpdateBlobs(const BatchUpdateBlobsRequest &request,
                                   BatchUpdateBlobsResponse *response)
{
    BUILDBOX_LOG_INFO(
        "LocalCasInstance::BatchUpdateBlobs request for instance name \""
        << request.instance_name() << "\" for " << request.requests().size()
        << " blob(s)");

    for (const auto &blob : request.requests()) {
        const google::rpc::Status status =
            writeToLocalStorage(blob.digest(), blob.data());

        auto entry = response->add_responses();
        entry->mutable_digest()->CopyFrom(blob.digest());
        entry->mutable_status()->CopyFrom(status);
    }

    buildboxcommon::buildboxcommonmetrics::CountingMetricUtil::
        recordCounterMetric(MetricNames::COUNTER_NUM_BLOBS_BATCH_UPDATE,
                            request.requests().size());

    return grpc::Status::OK;
}

grpc::Status
LocalCasInstance::BatchReadBlobs(const BatchReadBlobsRequest &request,
                                 BatchReadBlobsResponse *response)
{
    BUILDBOX_LOG_INFO(
        "LocalCasInstance::BatchReadBlobs request for instance name \""
        << request.instance_name() << "\" for " << request.digests().size()
        << " digest(s)");

    for (const Digest &digest : request.digests()) {
        std::string data;
        const google::rpc::Status status = readFromLocalStorage(digest, &data);

        auto entry = response->add_responses();
        entry->mutable_digest()->CopyFrom(digest);
        entry->mutable_status()->CopyFrom(status);
        entry->set_data(data);
    }

    buildboxcommon::buildboxcommonmetrics::CountingMetricUtil::
        recordCounterMetric(MetricNames::COUNTER_NUM_BLOBS_BATCH_READ,
                            response->responses_size());

    return grpc::Status::OK;
}

grpc::Status LocalCasInstance::Write(WriteRequest *request_message,
                                     ServerReader<WriteRequest> &request,
                                     WriteResponse *response,
                                     Digest *requested_digest)
{
    response->set_committed_size(0);

    auto buffer_file = d_storage->createTemporaryFile();

    Digest digest_to_write;
    auto request_status = CasInstance::processWriteRequest(
        request_message, request, &digest_to_write, buffer_file.name());

    if (request_status.error_code() != grpc::INVALID_ARGUMENT) {
        *requested_digest = digest_to_write;
    }

    if (!request_status.ok()) {
        if (request_status.error_code() == grpc::StatusCode::ALREADY_EXISTS) {
            // The blob is already present in the CAS. In that case, according
            // to the REAPI spec:
            // " [...] if another client has already completed the upload
            // [...], the request will terminate immediately with a response
            // whose `committed_size` is the full size of the uploaded file
            // (regardless of how much data was transmitted by the client)"
            response->set_committed_size(digest_to_write.size_bytes());
            return grpc::Status::OK;
        }

        return request_status;
    }

    // We can move the file directly to the CAS, avoiding copies:
    auto move_status = moveTemporaryFileToLocalStorage(digest_to_write,
                                                       std::move(buffer_file));

    if (move_status.ok()) {
        response->set_committed_size(digest_to_write.size_bytes());
    }

    return move_status;
}

google::rpc::Status
LocalCasInstance::writeToLocalStorage(const Digest &digest,
                                      const std::string &data)
{
    google::rpc::Status status;

    try {
        d_storage->writeBlob(digest, data);
        status.set_code(grpc::StatusCode::OK);
    }
    catch (const std::invalid_argument &) {
        status.set_code(grpc::StatusCode::INVALID_ARGUMENT);
        status.set_message(
            "The size of the data does not match the size defined "
            "in the digest.");
    }
    catch (const std::runtime_error &e) {
        status.set_code(grpc::StatusCode::INTERNAL);
        status.set_message("Internal error while writing blob to local CAS: " +
                           std::string(e.what()));
    }

    return status;
}

grpc::Status LocalCasInstance::moveTemporaryFileToLocalStorage(
    const Digest &digest, buildboxcommon::TemporaryFile &&tempFile)
{
    try {
        d_storage->moveBlobFromTemporaryFile(digest, std::move(tempFile));
        return grpc::Status::OK;
    }
    catch (const std::invalid_argument &e) {
        return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                            "Invalid argument: " + std::string(e.what()));
    }
    catch (const std::runtime_error &e) {
        return grpc::Status(grpc::StatusCode::INTERNAL,
                            "Internal error while moving blob to local CAS: " +
                                std::string(e.what()));
    }
}

google::rpc::Status
LocalCasInstance::readFromLocalStorage(const Digest &digest,
                                       std::string *data) const
{
    return readFromLocalStorage(digest, data, 0, 0);
}

google::rpc::Status
LocalCasInstance::readFromLocalStorage(const Digest &digest, std::string *data,
                                       size_t offset, size_t limit) const
{
    google::rpc::Status status;

    try {
        const size_t read_length = (limit > 0) ? limit : LocalCas::npos;
        const auto data_ptr = d_storage->readBlob(
            digest, static_cast<int64_t>(offset), read_length);
        if (data_ptr == nullptr) {
            status.set_code(grpc::StatusCode::NOT_FOUND);
            status.set_message("Blob not found in the local CAS.");
        }
        else {
            *data = std::move(*data_ptr);
            status.set_code(grpc::StatusCode::OK);
        }
    }
    catch (const std::out_of_range &) {
        status.set_code(grpc::StatusCode::INVALID_ARGUMENT);
        status.set_message("Read interval is out of range.");
    }
    catch (const std::runtime_error &e) {
        status.set_code(grpc::StatusCode::INTERNAL);
        status.set_message(
            "Internal error while fetching blob in local CAS: " +
            std::string(e.what()));
    }

    return status;
}

bool LocalCasInstance::hasBlob(const Digest &digest)
{
    return d_storage->hasBlob(digest);
}

google::rpc::Status LocalCasInstance::readBlob(const Digest &digest,
                                               std::string *data,
                                               size_t read_offset,
                                               size_t read_limit)
{
    return readFromLocalStorage(digest, data, read_offset, read_limit);
}

google::rpc::Status LocalCasInstance::writeBlob(const Digest &digest,
                                                const std::string &data)
{
    BUILDBOX_LOG_INFO("LocalCasInstance::writeBlob: writing digest of "
                      << digest.size_bytes() << " bytes");
    return writeToLocalStorage(digest, data);
}

std::expected<std::shared_ptr<Server>, grpc::Status>
LocalCasInstance::createNestedServer(
    const RemoteApisSocketConfig &config, Server *baseServer,
    const std::string &rootPath, const std::shared_ptr<InodeCache> &inodeCache)
{
    const bool hasInstanceName = !config.instance_name().empty();
    const bool hasInstanceMap = config.instance_map_size() > 0;

    if (hasInstanceName && hasInstanceMap) {
        return std::unexpected(
            Status(grpc::StatusCode::INVALID_ARGUMENT,
                   "Cannot specify both instance_name and instance_map"));
    }

    // Build the clone map: cloneName -> baseName.
    // The proto instance_map has the same mapping.
    // For single-instance mode, instance_name is the base, cloned to "".
    std::unordered_map<std::string, std::string> cloneMap;
    if (hasInstanceMap) {
        for (const auto &[cloneName, baseName] : config.instance_map()) {
            cloneMap.emplace(cloneName, baseName);
        }
    }
    else {
        cloneMap.emplace("", config.instance_name());
    }

    // Validate that all base instances exist.
    for (const auto &[cloneName, baseName] : cloneMap) {
        if (!baseServer->getInstanceManager()->contains(baseName)) {
            return std::unexpected(
                Status(grpc::StatusCode::INVALID_ARGUMENT,
                       "Invalid instance name \"" + baseName + "\""));
        }
    }

    return std::make_shared<Server>(baseServer, cloneMap, rootPath,
                                    config.action_cache_update_enabled(),
                                    inodeCache);
}

grpc::Status LocalCasInstance::StageTree(
    const StageTreeRequest &stage_request,
    ServerReaderWriter<StageTreeResponse, StageTreeRequest> *stream,
    Server *baseServer)
{
    if (chrooted()) {
        return Status(grpc::StatusCode::FAILED_PRECONDITION,
                      "Staging not available in sandboxed instance");
    }

    // Before we can stage the tree, we need to make sure that all of its
    // contents are stored locally.
    auto tree_availability_status =
        prepareTreeForStaging(stage_request.root_digest());
    if (!tree_availability_status.ok()) {
        return tree_availability_status;
    }
    // All the blobs required to stage are present in the local storage.

    /* There are 3 cases for `stage_request.path()`:
     *  a) Empty string (we need to create a temporary directory),
     *  b) A path to a directory that doesn't exist yet
     *  (and that will be created by the stager), or
     *  c) A path to an empty directory.
     *
     * In cases a) and b), cleanup involves deleting the directory. But for c)
     * we just want to empty it.
     */

    bool stage_directory_needs_deleting = false;
    std::string stage_path;
    if (stage_request.path().empty()) {
        try {
            stage_path = createStagingDirectory();
            stage_directory_needs_deleting = true;
        }
        catch (const std::system_error &e) {
            std::ostringstream error_message;
            error_message << "Could not create temporary directory to stage: "
                          << stage_request.root_digest() << ": " << e.what();

            BUILDBOX_LOG_ERROR(error_message.str());
            return Status(grpc::StatusCode::INTERNAL, error_message.str());
        }
    }
    else {
        stage_path = stage_request.path();

        const bool stage_directory_exists =
            buildboxcommon::FileUtils::isDirectory(
                stage_request.path().c_str());

        // If the directory does not exist, we create it and will need
        // to delete it later:
        if (!stage_directory_exists) {
            buildboxcommon::FileUtils::createDirectory(
                stage_request.path().c_str());
            stage_directory_needs_deleting = true;
        }
    }

    FileStagedCallback fileStagedCallback;
    std::shared_ptr<InodeCache> inodeCache;
    const auto &remoteApisSocket = stage_request.remote_apis_socket();
    if (!remoteApisSocket.path().empty()) {
        // Prepopulate inode cache for staged files
        inodeCache = std::make_shared<InodeCache>();
        fileStagedCallback = [&](struct stat st, const Digest &digest) {
            inodeCache->addInode(st, digest);
        };
    }

    // Stage the files with the stage method which will also write back
    // onto the stream to the client to inform them that the staging is
    // done.
    Status stage_status;
    std::unique_ptr<FileStager::StagedDirectory> staged_directory;
    std::shared_ptr<Server> server;
    {
        std::optional<ProcessCredentials> accessCredentials;
        if (stage_request.has_access_credentials()) {
            const int64_t uid = stage_request.access_credentials().uid();
            if (uid < 0 || uid >= (uid_t)-1) {
                return Status(grpc::StatusCode::INVALID_ARGUMENT,
                              "Invalid uid in Credentials message");
            }
            const int64_t gid = stage_request.access_credentials().gid();
            if (gid < 0 || gid >= (gid_t)-1) {
                return Status(grpc::StatusCode::INVALID_ARGUMENT,
                              "Invalid gid in Credentials message");
            }
            accessCredentials =
                ProcessCredentials{.uid = static_cast<uid_t>(uid),
                                   .gid = static_cast<uid_t>(gid)};
        }

        buildboxcommon::buildboxcommonmetrics::MetricGuard<
            buildboxcommon::buildboxcommonmetrics::DurationMetricTimer>
            mt(MetricNames::TIMER_NAME_LOCAL_CAS_STAGE_TREE_STAGE);

        auto stager =
            getFileStager(stage_request.staging_mode(), accessCredentials);
        if (stager == nullptr) {
            stage_status = Status(grpc::StatusCode::INVALID_ARGUMENT,
                                  "Unsupported staging mode: " +
                                      StageTreeRequest_StagingMode_Name(
                                          stage_request.staging_mode()));
            return stage_status;
        }

        try {
            staged_directory =
                stager->stage(stage_request.root_digest(), stage_path,
                              accessCredentials, fileStagedCallback);

            // Create an additional, restricted server, if requested
            if (!remoteApisSocket.path().empty()) {
                auto nestedServer = createNestedServer(
                    remoteApisSocket, baseServer, stage_path, inodeCache);
                if (!nestedServer.has_value()) {
                    return nestedServer.error();
                }
                server = std::move(*nestedServer);

                std::optional<gid_t> socketGid;
                std::optional<mode_t> socketMode;

                if (accessCredentials.has_value() &&
                    accessCredentials->uid != d_uid) {
                    // Access by a different user
                    if (SystemUtils::getGids().count(accessCredentials->gid) >
                        0) {
                        // Grant socket access to shared group
                        socketGid = accessCredentials->gid;
                        socketMode = PERMISSION_RWGRP;
                    }
                    else {
                        // No shared group, grant socket access to everyone
                        // with access to the parent directory
                        socketMode = PERMISSION_RWALL;
                    }
                }

                startServerWithUnixSocket(server, stage_path,
                                          remoteApisSocket.path(), socketGid,
                                          socketMode);
            }

            StageTreeResponse response;
            response.set_path(stage_path);
            stream->Write(response);

            stage_status = grpc::Status::OK;
        }
        catch (const std::invalid_argument &e) {
            stage_status =
                Status(grpc::StatusCode::INVALID_ARGUMENT, e.what());
        }
        catch (const std::runtime_error &e) {
            stage_status = Status(grpc::StatusCode::INTERNAL, e.what());
        }

        if (!stage_status.ok()) {
            if (stage_directory_needs_deleting) {
                // `stage()` rolls back the status of `stage_path` when the
                // operation aborts, leaving it as it originally was.
                // However, if we created a temporary directory, we want to
                // delete it.
                try {
                    buildboxcommon::FileUtils::deleteDirectory(
                        stage_path.c_str());
                }
                catch (const std::system_error &e) {
                    BUILDBOX_LOG_ERROR("Could not delete directory "
                                       << stage_path << ":" << e.what());
                }
            }

            return stage_status;
        }
    }

    // The staging operation was successful.
    // Now we wait for a second request, which we expect to be empty:
    StageTreeRequest cleanup_request;
    const bool received_cleanup_request = stream->Read(&cleanup_request);

    if (received_cleanup_request &&
        (!cleanup_request.path().empty() ||
         !(cleanup_request.root_digest() == Digest()))) {
        std::ostringstream error_message;
        error_message << "Unexpected non-empty request after staging "
                      << stage_request.root_digest() << " in " << stage_path;
        stage_status =
            Status(grpc::StatusCode::INVALID_ARGUMENT, error_message.str());
    }

    if (server) {
        server->shutdown();
        server->wait();
    }

    // The client doesn't block on the RPC after sending the request so these
    // commands are non-blocking. See
    // `CASClient::StagedDirectory::~StagedDirectory()`
    for (const auto &command : stage_request.pre_unstage_commands()) {
        if (!d_allowedPreUnstageCommands->contains(command)) {
            BUILDBOX_LOG_WARNING("pre-unstage command [" << command
                                                         << "] not allowed");
            continue;
        }

        BUILDBOX_LOG_DEBUG("Running pre-unstage command [" << command << "]");
        try {
            // `executeCommandAndWait` throws if it fails to fork
            const int rc = SystemUtils::executeCommandAndWait(
                {command, stage_path}, true);
            if (rc != 0) {
                BUILDBOX_LOG_WARNING("pre-unstage command ["
                                     << command << "] failed rc=" << rc
                                     << " path=" << stage_path);
            }
        }
        catch (std::exception &e) {
            BUILDBOX_LOG_ERROR("Failed to launch pre-unstage command="
                               << command << " error=" << e.what());
        }
    }

    // The `StagedDirectory` object's destructor will perform the
    // unstaging, clearing the directory's contents.
    staged_directory.reset();

    try {
        // If we created the top-level directory, we delete it as well.
        if (stage_directory_needs_deleting) {
            buildboxcommon::FileUtils::deleteDirectory(stage_path.c_str());
        }
    }
    catch (const std::system_error &e) {
        BUILDBOX_LOG_WARNING("Caught " << e.what()
                                       << " while removing directory ["
                                       << stage_path);
    }

    // Sending an empty reply that signals that we are done cleaning:
    if (received_cleanup_request) {
        stream->Write(StageTreeResponse());
    }

    return stage_status;
}

grpc::Status LocalCasInstance::NestedServer(
    const NestedServerRequest &request,
    ServerReaderWriter<NestedServerResponse, NestedServerRequest> *stream,
    Server *baseServer)
{
    if (chrooted()) {
        return Status(grpc::StatusCode::FAILED_PRECONDITION,
                      "Cannot create nested server in sandboxed instance");
    }

    if (request.path().empty() || request.path()[0] != '/') {
        return Status(grpc::StatusCode::INVALID_ARGUMENT,
                      "Not an absolute path: '" + request.path() + "'");
    }
    if (!buildboxcommon::FileUtils::isDirectory(request.path().c_str())) {
        return Status(grpc::StatusCode::FAILED_PRECONDITION,
                      "Not a directory: '" + request.path() + "'");
    }

    const auto &remoteApisSocket = request.remote_apis_socket();
    if (remoteApisSocket.path().empty()) {
        return Status(grpc::StatusCode::INVALID_ARGUMENT,
                      "Socket path missing");
    }

    std::shared_ptr<Server> server;
    std::optional<ProcessCredentials> accessCredentials;
    if (request.has_access_credentials()) {
        const int64_t uid = request.access_credentials().uid();
        if (uid < 0 || uid >= (uid_t)-1) {
            return Status(grpc::StatusCode::INVALID_ARGUMENT,
                          "Invalid uid in Credentials message");
        }
        const int64_t gid = request.access_credentials().gid();
        if (gid < 0 || gid >= (gid_t)-1) {
            return Status(grpc::StatusCode::INVALID_ARGUMENT,
                          "Invalid gid in Credentials message");
        }
        accessCredentials = ProcessCredentials{.uid = static_cast<uid_t>(uid),
                                               .gid = static_cast<uid_t>(gid)};
    }

    try {
        auto nestedServer =
            createNestedServer(remoteApisSocket, baseServer, request.path(),
                               std::make_shared<InodeCache>());
        if (!nestedServer.has_value()) {
            return nestedServer.error();
        }
        server = std::move(*nestedServer);

        std::optional<gid_t> socketGid;
        std::optional<mode_t> socketMode;

        if (accessCredentials.has_value() && accessCredentials->uid != d_uid) {
            // Access by a different user
            if (SystemUtils::getGids().count(accessCredentials->gid) > 0) {
                // Grant socket access to shared group
                socketGid = accessCredentials->gid;
                socketMode = PERMISSION_RWGRP;
            }
            else {
                // No shared group, grant socket access to everyone
                // with access to the parent directory
                socketMode = PERMISSION_RWALL;
            }
        }

        startServerWithAbsoluteUnixSocket(
            server, request.path() + "/" + remoteApisSocket.path(), socketGid,
            socketMode);

        stream->Write(NestedServerResponse());
    }
    catch (const std::invalid_argument &e) {
        return Status(grpc::StatusCode::INVALID_ARGUMENT, e.what());
    }
    catch (const std::runtime_error &e) {
        return Status(grpc::StatusCode::INTERNAL, e.what());
    }

    // The nested server creation was successful.
    // Now we wait for a second request, which we expect to be empty:
    NestedServerRequest shutdownRequest;
    const bool receivedShutdownRequest = stream->Read(&shutdownRequest);

    if (server) {
        server->shutdown();
        server->wait();
    }

    // Sending an empty reply that signals that we are done with the shutdown:
    if (receivedShutdownRequest) {
        stream->Write(NestedServerResponse());

        if (!shutdownRequest.path().empty()) {
            return Status(
                grpc::StatusCode::INVALID_ARGUMENT,
                "Unexpected non-empty request after nested server creation");
        }
    }

    return grpc::Status::OK;
}

grpc::Status LocalCasInstance::ExportFiles(const ExportFilesRequest &request,
                                           ExportFilesResponse *response)
{
    buildboxcommon::buildboxcommonmetrics::MetricGuard<
        buildboxcommon::buildboxcommonmetrics::DurationMetricTimer>
        mt(MetricNames::TIMER_NAME_LOCAL_CAS_EXPORT_FILES);

    FileDescriptor destRootDirfd(FileUtils::openInRoot(
        rootDirfd(), request.path().empty() ? "/" : request.path(),
        O_RDONLY | O_DIRECTORY | O_CLOEXEC));
    if (destRootDirfd.get() < 0) {
        return Status(grpc::StatusCode::FAILED_PRECONDITION,
                      "Failed to open destination directory: " +
                          std::string(std::strerror(errno)));
    }

    // Don't use inode cache for (long-lived) top-level instances as
    // there is currently no expiry mechanism.
    const bool addToInodeCache = (bool)d_inodeCache;

    for (const auto &file : request.output_files()) {
        auto resp = response->add_responses();
        resp->set_path(file.path());

        if (file.node_properties().ByteSizeLong() > 0) {
            resp->mutable_status()->set_code(grpc::StatusCode::UNIMPLEMENTED);
            resp->mutable_status()->set_message(
                "Exporting files with node properties is not currently "
                "supported");
        }
        else if (!d_storage->hasBlob(file.digest())) {
            resp->mutable_status()->set_code(grpc::StatusCode::NOT_FOUND);
        }
        else {
            const auto destPath = std::filesystem::path(file.path());
            const auto parentPath = destPath.parent_path();
            FileDescriptor destDirfd(FileUtils::openInRoot(
                destRootDirfd.get(), parentPath.empty() ? "." : parentPath,
                O_RDONLY | O_DIRECTORY | O_CLOEXEC));
            if (destDirfd.get() < 0) {
                resp->mutable_status()->set_code(
                    grpc::StatusCode::FAILED_PRECONDITION);
                resp->mutable_status()->set_message(
                    "Failed to open destination directory: " +
                    std::string(std::strerror(errno)));
                continue;
            }
            const mode_t mode = file.is_executable() ? PERMISSION_RWXUSR_RXALL
                                                     : PERMISSION_RWUSR_RALL;
            try {
                struct stat st{};

                if (!d_storage->copyBlob(file.digest(), destDirfd.get(),
                                         destPath.filename().c_str(), mode,
                                         addToInodeCache ? &st : nullptr)) {
                    resp->mutable_status()->set_code(
                        grpc::StatusCode::NOT_FOUND);
                    continue;
                }

                if (addToInodeCache) {
                    d_inodeCache->addInode(st, file.digest());
                }
            }
            catch (const std::system_error &e) {
                resp->mutable_status()->set_code(
                    grpc::StatusCode::FAILED_PRECONDITION);
                resp->mutable_status()->set_message(e.what());
            }
        }
    }

    return grpc::Status::OK;
}

bool LocalCasInstance::treeIsAvailableLocally(const Digest &root_digest,
                                              bool file_blobs) const
{
    int64_t cache_hits = 0;
    int64_t cache_misses = 0;

    const bool ret = treeIsAvailableLocally(root_digest, file_blobs,
                                            &cache_hits, &cache_misses);

    recordTreeCacheMetrics(cache_hits, cache_misses);

    return ret;
}

bool LocalCasInstance::treeIsAvailableLocally(const Digest &root_digest,
                                              bool file_blobs,
                                              int64_t *cache_hits,
                                              int64_t *cache_misses) const
{
    auto tree_cache = this->getTreeCache();
    if (tree_cache->hasRootDigest(root_digest, file_blobs)) {
        (*cache_hits)++;
        return true;
    }

    (*cache_misses)++;

    Directory directory;
    if (!d_storage->hasBlob(root_digest) ||
        !directory.ParseFromString(*d_storage->readBlob(root_digest))) {
        return false;
    }

    if (file_blobs) {
        for (const FileNode &file : directory.files()) {
            if (!this->d_storage->hasBlob(file.digest())) {
                return false;
            }
        }
    }

    for (const DirectoryNode &dir : directory.directories()) {
        if (!treeIsAvailableLocally(dir.digest(), file_blobs, cache_hits,
                                    cache_misses)) {
            return false;
        }
    }

    tree_cache->addRootDigest(root_digest, file_blobs);

    return true;
}

grpc::Status
LocalCasInstance::prepareTreeForStaging(const Digest &root_digest) const
{
    // Server mode. (All the blobs must be available locally.)
    if (treeIsAvailableLocally(root_digest)) {
        return grpc::Status::OK;
    }
    return Status(grpc::StatusCode::FAILED_PRECONDITION,
                  "Tree is not completely available from LocalCAS.");
}

std::vector<Digest>
LocalCasInstance::digestsMissingFromDirectory(const Directory &directory,
                                              bool file_blobs) const
{
    std::vector<Digest> missing_digests;
    missing_digests.reserve(static_cast<size_t>(directory.files_size()) +
                            static_cast<size_t>(directory.directories_size()));

    if (file_blobs) {
        for (const FileNode &file : directory.files()) {
            if (!this->d_storage->hasBlob(file.digest())) {
                missing_digests.push_back(file.digest());
            }
        }
    }
    for (const DirectoryNode &dir : directory.directories()) {
        if (!this->d_storage->hasBlob(dir.digest())) {
            missing_digests.push_back(dir.digest());
        }
    }
    missing_digests.shrink_to_fit();
    return missing_digests;
}

void LocalCasInstance::startServerWithUnixSocket(
    const std::shared_ptr<Server> &server,
    const std::filesystem::path &rootPath,
    const std::filesystem::path &socketPath, std::optional<gid_t> gid,
    std::optional<mode_t> mode)
{
    if (!socketPath.is_relative()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument,
            "Socket path must be relative to input root: " << socketPath);
    }

    // Canonicalize the socket directory (resolving `..` and symlinks)
    // to verify that the socket directory is inside the staged
    // directory. As this is part of the stage operation, potentially
    // sandboxed commands haven't started execution and thus, there is
    // no race condition with sandboxed commands manipulating
    // directory symlinks.
    auto canonicalStagePath = std::filesystem::canonical(rootPath);
    auto canonicalSocketDirectory = std::filesystem::canonical(
        canonicalStagePath / socketPath.parent_path());
    if (std::mismatch(canonicalStagePath.begin(), canonicalStagePath.end(),
                      canonicalSocketDirectory.begin())
            .first != canonicalStagePath.end()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument,
            "Socket path must not escape the input root: " << socketPath);
    }

    // `bind()` for UNIX sockets doesn't support directory file
    // descriptors and the path length is severely limited.
    // Use a symlink from a temporary directory to work around the
    // path length limitation.
    buildboxcommon::TemporaryDirectory hostTempDirectory;
    if (symlink(canonicalSocketDirectory.c_str(),
                (hostTempDirectory.strname() + "/stage").c_str()) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to create directory symlink for socket");
    }

    const std::string tempSocketPath = hostTempDirectory.strname() +
                                       "/stage/" +
                                       socketPath.filename().string();
    server->addListeningPort("unix:" + tempSocketPath);
    server->start();

    if (gid.has_value()) {
        if (chown(tempSocketPath.c_str(), -1, gid.value()) != 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Failed to chown unix socket at "
                    << tempSocketPath << ", error: " << std::strerror(errno));
        }
    }
    if (mode.has_value()) {
        if (chmod(tempSocketPath.c_str(), mode.value()) != 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Failed to chmod unix socket at "
                    << tempSocketPath << ", error: " << std::strerror(errno));
        }
    }
}

void LocalCasInstance::startServerWithAbsoluteUnixSocket(
    const std::shared_ptr<Server> &server,
    const std::filesystem::path &socketPath, std::optional<gid_t> gid,
    std::optional<mode_t> mode)
{
    if (!socketPath.is_absolute()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument,
            "Socket path must be absolute: " << socketPath);
    }

    BUILDBOX_LOG_DEBUG("Starting server at " << socketPath);

    server->addListeningPort("unix:" + socketPath.string());
    server->start();

    if (gid.has_value()) {
        if (chown(socketPath.c_str(), -1, gid.value()) != 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Failed to chown unix socket at "
                    << socketPath << ", error: " << std::strerror(errno));
        }
    }
    if (mode.has_value()) {
        if (chmod(socketPath.c_str(), mode.value()) != 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Failed to chmod unix socket at "
                    << socketPath << ", error: " << std::strerror(errno));
        }
    }
}

std::string LocalCasInstance::createStagingDirectory() const
{
    buildboxcommon::TemporaryDirectory stage_directory =
        d_storage->createStagingDirectory();
    stage_directory.setAutoRemove(false);
    return std::string(stage_directory.name());
}

Digest LocalCasInstance::cacheFile(int fd) { return cache(fd, true); }

Digest LocalCasInstance::cacheInode(int fd) { return cache(fd, false); }

Digest LocalCasInstance::cache(int fd, bool writeBlob)
{
    struct stat st{};

    if (d_inodeCache) {
        // Don't use inode cache for (long-lived) top-level instances as
        // there is currently no expiry mechanism.

        if (fstat(fd, &st) < 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "cacheFile: Failed to stat file");
        }

        const auto cachedDigest = d_inodeCache->getDigest(st);
        if (cachedDigest.has_value() &&
            (!writeBlob || d_storage->hasBlob(*cachedDigest))) {
            buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
                MetricNames::COUNTER_NAME_LOCAL_CAS_INODE_CACHE_HITS, 1);
            return *cachedDigest;
        }
        else {
            buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
                MetricNames::COUNTER_NAME_LOCAL_CAS_INODE_CACHE_MISSES, 1);
        }
    }

    Digest digest;

    if (writeBlob) {
        d_storage->writeBlob(fd, &digest);
    }
    else {
        digest = DigestGenerator::hash(fd);
    }

    if (d_inodeCache) {
        d_inodeCache->addInode(st, digest);
    }

    return digest;
}

Digest LocalCasInstance::cacheInodeXattr(int fd, const std::string &xattrName)
{
    return cacheXattr(fd, xattrName, false);
}

Digest LocalCasInstance::cacheFileXattr(int fd, const std::string &xattrName)
{
    return cacheXattr(fd, xattrName, true);
}

Digest LocalCasInstance::cacheXattr(int fd, const std::string &xattrName,
                                    bool writeBlob)
{
#ifndef __linux__
    if (writeBlob) {
        return cacheFile(fd);
    }
    else {
        return cacheInode(fd);
    }
#else
    // Support hexadecimal hashes up to 512 bits
    const int maxHashSize = 512 / 4;
    char hash[maxHashSize];
    ssize_t xattrSize = fgetxattr(fd, xattrName.c_str(), hash, sizeof(hash));

    if (xattrSize < 0) {
        if (errno == ENODATA) {
            // Hash is not available
            if (writeBlob) {
                return cacheFile(fd);
            }
            else {
                return cacheInode(fd);
            }
        }
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(std::system_error, errno,
                                              std::system_category,
                                              "Failed to get xattr");
    }

    struct stat st{};
    if (fstat(fd, &st) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "cacheFileXattr: Failed to stat file");
    }

    Digest digest;
    digest.set_hash(std::string(hash, xattrSize));
    digest.set_size_bytes(st.st_size);
    if (writeBlob && !d_storage->hasBlob(digest)) {
        // This should be rare as the blob was available at time of staging.
        return cacheFile(fd);
    }
    return digest;
#endif
}

bool LocalCasInstance::hasRemote() const { return false; }

grpc::Status LocalCasInstance::CaptureTree(const CaptureTreeRequest &request,
                                           CaptureTreeResponse *response)
{
    const std::vector<std::string> capture_properties(
        request.node_properties().begin(), request.node_properties().end());

    if (request.skip_upload() && request.bypass_local_cache()) {
        return grpc::Status(
            grpc::StatusCode::INVALID_ARGUMENT,
            "Only one of `skip_upload` and `bypass_local_cache` can be set.");
    }

    const bool upload = hasRemote() && !request.skip_upload();
    const bool bypass_local_cache = upload && request.bypass_local_cache();

    for (const std::string &path : request.path()) {
        auto entry = response->add_responses();
        *entry = captureDirectory(
            request.root(), path, capture_properties, bypass_local_cache,
            request.move_files(), request.output_directory_format(),
            request.skip_upload(),
            static_cast<mode_t>(request.unix_mode_mask().value()),
            request.allow_chmod_to_read());
    }

    return grpc::Status::OK;
}

CaptureTreeResponse_Response LocalCasInstance::captureDirectory(
    const std::string &root, const std::string &path,
    const std::vector<std::string> &capture_properties,
    const bool bypass_local_cache, bool move_files_hint,
    const Command_OutputDirectoryFormat outputDirFormat, const bool skipUpload,
    const mode_t unixModeMask, const bool allowChmodToRead)
{
    CaptureTreeResponse_Response response;

    google::rpc::Status status;

    if ((!root.empty() && root[0] != '/') ||
        (root.empty() && !path.empty() && path[0] != '/')) {
        logAndStoreMessage(grpc::StatusCode::INVALID_ARGUMENT,
                           (root.empty() ? path : root) +
                               " is not an absolute path.",
                           &status);
        *response.mutable_status() = std::move(status);
        return response;
    }

    if (chrooted() || !root.empty() ||
        !d_storage->externalFileMovesAllowed()) {
        // Moving files is incompatible with safe path resolution in root.
        move_files_hint = false;
    }

    std::optional<Tree> tree_message;
    Digest tree_digest;
    Digest directory_digest;

    FileDescriptor dirfd(
        this->openInRoot(root, path, O_RDONLY | O_DIRECTORY | O_CLOEXEC));
    if (dirfd.get() < 0) {
        logAndStoreMessage(grpc::StatusCode::NOT_FOUND,
                           "Could not open directory " + path + " in " + root,
                           &status);
        *response.mutable_status() = std::move(status);
        return response;
    }

    FileStager *stager =
        getFileStager(StageTreeRequest_StagingMode_DEFAULT, std::nullopt);
    std::string xattrName = stager->getHashXattrName(dirfd.get());
    if (!xattrName.empty()) {
        // The extended attribute is available only on FUSE filesystems.
        // As zero-copy file moves across filesystems are impossible, switch to
        // the regular code path, which is faster when moves are not possible.
        move_files_hint = false;
    }

    MerklizeResult merklizeResult;
    try {
        UnixModeUpdater unixModeUpdater;
        if (unixModeMask != 0) {
            unixModeUpdater = unixModeMaskUpdater(unixModeMask);
        }

        Merklizer merklizer(false, capture_properties, nullptr,
                            d_digestThreadPool.get());
        if (!bypass_local_cache && !move_files_hint) {
            if (!xattrName.empty()) {
                merklizeResult = merklizer.merklize(
                    dirfd.get(), "",
                    [&](int fd) { return cacheFileXattr(fd, xattrName); },
                    unixModeUpdater, {}, allowChmodToRead);
            }
            else {
                merklizeResult = merklizer.merklize(
                    dirfd.get(), "", [&](int fd) { return cacheFile(fd); },
                    unixModeUpdater, {}, allowChmodToRead);
            }
        }
        else {
            merklizeResult =
                merklizer.merklize(dirfd.get(), "", Merklizer::hashFile,
                                   unixModeUpdater, {}, allowChmodToRead);
        }
        if (outputDirFormat != Command_OutputDirectoryFormat_DIRECTORY_ONLY) {
            tree_message = {merklizeResult.tree()};
        }
        directory_digest = merklizeResult.d_rootDigest;
    }
    catch (const OutOfSpaceException &e) {
        const auto error_message =
            "Out of space error in `merklize()` for path \"" + path +
            "\": " + e.what();
        logAndStoreMessage(grpc::StatusCode::RESOURCE_EXHAUSTED, error_message,
                           &status);
        *response.mutable_status() = std::move(status);
        return response;
    }
    // Catch system errors thrown in `merklize()`.
    catch (const std::system_error &e) {
        const auto error_message = "System error in `merklize()` for path \"" +
                                   path + "\": " + e.what();
        logAndStoreMessage(grpc::StatusCode::INTERNAL, error_message, &status);
        *response.mutable_status() = std::move(status);
        return response;
    }

    int64_t total_size = 0;
    for (const auto &[digest, path] : merklizeResult.d_digestToPath) {
        total_size += digest.size_bytes();
    }
    response.set_total_size_bytes(total_size);

    // Upload to remote CAS and store locally if `bypass_local_cache` is
    // not set
    try {
        if (skipUpload) {
            tree_digest = LocalCasInstance::UploadAndStore(
                dirfd.get(), merklizeResult.d_digestToDirectoryBlob,
                merklizeResult.d_digestToPath, tree_message,
                bypass_local_cache, move_files_hint);
        }
        else {
            tree_digest = UploadAndStore(
                dirfd.get(), merklizeResult.d_digestToDirectoryBlob,
                merklizeResult.d_digestToPath, tree_message,
                bypass_local_cache, move_files_hint);
        }

        const auto number_of_blobs_captured = static_cast<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue::Count>(
            merklizeResult.d_digestToDirectoryBlob.size() +
            merklizeResult.d_digestToPath.size());
        buildboxcommon::buildboxcommonmetrics::CountingMetricUtil::
            recordCounterMetric(MetricNames::COUNTER_NUM_BLOBS_CAPTURED,
                                number_of_blobs_captured);

        status.set_code(grpc::StatusCode::OK);
    }
    catch (const OutOfSpaceException &e) {
        const auto error_message =
            "Out of space error in `writeBlob()` for path \"" + path +
            "\": " + e.what();
        logAndStoreMessage(grpc::StatusCode::RESOURCE_EXHAUSTED, error_message,
                           &status);
    }
    catch (const std::runtime_error &e) {
        const auto error_message =
            "Runtime error uploading and storing path \"" + path +
            "\": " + e.what();
        logAndStoreMessage(grpc::StatusCode::INTERNAL, error_message, &status);
    }
    catch (const std::invalid_argument &e) {
        const auto error_message =
            "Invalid argument error uploading and storing path \"" + path +
            "\": " + e.what();
        logAndStoreMessage(grpc::StatusCode::INTERNAL, error_message, &status);
    }
    if (status.code() == grpc::StatusCode::OK) {
        if (outputDirFormat != Command_OutputDirectoryFormat_DIRECTORY_ONLY) {
            response.mutable_tree_digest()->CopyFrom(tree_digest);
        }
        if (outputDirFormat != Command_OutputDirectoryFormat_TREE_ONLY) {
            response.mutable_root_directory_digest()->CopyFrom(
                directory_digest);
        }

        response.set_path(path);
    }

    *response.mutable_status() = std::move(status);
    return response;
}

grpc::Status LocalCasInstance::HashFiles(const HashFilesRequest &request,
                                         HashFilesResponse *response)
{
    std::vector<std::string> capture_properties(
        request.node_properties().cbegin(), request.node_properties().cend());

    for (const std::string &path : request.path()) {
        auto entry = response->add_responses();
        *entry =
            HashFile(request.root(), path, capture_properties,
                     static_cast<mode_t>(request.unix_mode_mask().value()),
                     request.allow_chmod_to_read());
    }

    return grpc::Status::OK;
}

HashFilesResponse_Response
LocalCasInstance::HashFile(const std::string &root, const std::string &path,
                           const std::vector<std::string> &capture_properties,
                           const mode_t unixModeMask,
                           const bool allowChmodToRead)
{
    HashFilesResponse_Response response;

    response.set_path(path);

    if ((!root.empty() && root[0] != '/') ||
        (root.empty() && !path.empty() && path[0] != '/')) {
        google::rpc::Status status;
        logAndStoreMessage(grpc::StatusCode::INVALID_ARGUMENT,
                           "Path: " + path + " is not an absolute path.",
                           &status);
        *response.mutable_status() = std::move(status);
        return response;
    }

    File file;
    FileDescriptor fd(this->openInRoot(root, path, O_RDONLY | O_CLOEXEC));
    if (fd.get() < 0) {
        google::rpc::Status status;
        BUILDBOX_LOG_DEBUG("Used fd: [" << rootDirfd() << "|" << root << "]");
        logAndStoreMessage(
            errno == ENOENT   ? grpc::StatusCode::NOT_FOUND
            : errno == EACCES ? grpc::StatusCode::PERMISSION_DENIED
                              : grpc::StatusCode::INTERNAL,
            "Could not open file " + path + " (" + strerror(errno) + ")",
            &status);
        *response.mutable_status() = std::move(status);
        return response;
    }
    if (FileUtils::isDirectory(fd.get())) {
        google::rpc::Status status;
        logAndStoreMessage(grpc::StatusCode::NOT_FOUND,
                           "Path is a directory: " + path, &status);
        *response.mutable_status() = std::move(status);
        return response;
    }

    UnixModeUpdater unixModeUpdater;
    if (unixModeMask != 0) {
        unixModeUpdater = unixModeMaskUpdater(unixModeMask);
    }

    FileStager *stager =
        getFileStager(StageTreeRequest_StagingMode_DEFAULT, std::nullopt);
    std::string xattrName = stager->getHashXattrName(fd.get());

    if (!xattrName.empty()) {
        file = File(
            fd.get(), [&](int fd) { return cacheInodeXattr(fd, xattrName); },
            capture_properties, unixModeUpdater, {}, allowChmodToRead);
    }
    else {
        file = File(
            fd.get(), [&](int fd) { return cacheInode(fd); },
            capture_properties, unixModeUpdater, {}, allowChmodToRead);
    }

    response.mutable_status()->set_code(grpc::StatusCode::OK);
    response.mutable_digest()->CopyFrom(file.d_digest);
    response.set_is_executable(file.d_executable);
    if (file.d_nodeProperties.ByteSizeLong() > 0) {
        response.mutable_node_properties()->CopyFrom(file.d_nodeProperties);
    }

    return response;
}

grpc::Status LocalCasInstance::CaptureFiles(const CaptureFilesRequest &request,
                                            CaptureFilesResponse *response)
{
    // Currently only support MTime but send all properties to File constructor
    const std::vector<std::string> capture_properties(
        request.node_properties().cbegin(), request.node_properties().cend());

    const bool upload = hasRemote();
    const bool bypass_local_cache = upload && request.bypass_local_cache();

    for (const std::string &path : request.path()) {
        auto entry = response->add_responses();
        *entry = captureFile(
            request.root(), path, capture_properties, bypass_local_cache,
            request.move_files(), request.skip_upload(),
            static_cast<mode_t>(request.unix_mode_mask().value()),
            request.allow_chmod_to_read());
    }

    return grpc::Status::OK;
}

void LocalCasInstance::captureFileData(const Digest &digest, int fd,
                                       const std::string &path, const bool,
                                       const bool move_file_hint)
{
    // fd is required, path may be empty
    if (fd < 0) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::invalid_argument,
                                       "captureFileData(): Invalid fd " << fd);
    }

    return addCapturedFileToLocalStorage(digest, fd, AT_FDCWD, path,
                                         move_file_hint);
}

void LocalCasInstance::addCapturedFileToLocalStorage(const Digest &digest,
                                                     int fd, int dirfd,
                                                     const std::string &path,
                                                     const bool move_file_hint)
{
    if (move_file_hint && !path.empty() &&
        d_storage->externalFileMovesAllowed()) {
        try {
            if (d_storage->moveBlobFromExternalFile(digest, dirfd, path)) {
                return;
            }
        }
        catch (const std::runtime_error &e) {
            BUILDBOX_LOG_DEBUG("Moving blob with digest ["
                               << digest
                               << "] failed (will try copying it instead): "
                               << e.what());
        }
        // If moving did not work for any reason, fall back to copying.
    }

    copyToLocalStorage(digest, fd, dirfd, path);
}

CaptureFilesResponse_Response LocalCasInstance::captureFile(
    const std::string &root, const std::string &path,
    const std::vector<std::string> &capture_properties,
    const bool bypass_local_cache, bool move_file_hint, const bool skipUpload,
    const mode_t unixModeMask, const bool allowChmodToRead)
{
    CaptureFilesResponse_Response response;

    response.set_path(path);

    if ((!root.empty() && root[0] != '/') ||
        (root.empty() && !path.empty() && path[0] != '/')) {
        google::rpc::Status status;
        logAndStoreMessage(grpc::StatusCode::INVALID_ARGUMENT,
                           "Path: " + path + " is not an absolute path.",
                           &status);
        *response.mutable_status() = std::move(status);
        return response;
    }

    if (!root.empty() || !d_storage->externalFileMovesAllowed()) {
        // Moving files is incompatible with safe path resolution in root.
        move_file_hint = false;
    }

    File file;
    FileDescriptor fd(this->openInRoot(root, path, O_RDONLY | O_CLOEXEC));
    if (fd.get() < 0) {
        google::rpc::Status status;
        logAndStoreMessage(
            errno == ENOENT   ? grpc::StatusCode::NOT_FOUND
            : errno == EACCES ? grpc::StatusCode::PERMISSION_DENIED
                              : grpc::StatusCode::INTERNAL,
            "Could not open file " + path + " (" + strerror(errno) + ")",
            &status);
        *response.mutable_status() = std::move(status);
        return response;
    }
    if (FileUtils::isDirectory(fd.get())) {
        google::rpc::Status status;
        logAndStoreMessage(grpc::StatusCode::NOT_FOUND,
                           "Path is a directory: " + path, &status);
        *response.mutable_status() = std::move(status);
        return response;
    }

    FileStager *stager =
        getFileStager(StageTreeRequest_StagingMode_DEFAULT, std::nullopt);
    std::string xattrName = stager->getHashXattrName(fd.get());
    if (!xattrName.empty()) {
        // The extended attribute is available only on FUSE filesystems.
        // As zero-copy file moves across filesystems are impossible, switch to
        // the regular code path, which is faster when moves are not possible.
        move_file_hint = false;
    }

    try {
        UnixModeUpdater unixModeUpdater;
        if (unixModeMask != 0) {
            unixModeUpdater = unixModeMaskUpdater(unixModeMask);
        }
        if (!bypass_local_cache && !move_file_hint) {
            if (!xattrName.empty()) {
                file = File(
                    fd.get(),
                    [&](int fd) { return cacheFileXattr(fd, xattrName); },
                    capture_properties, unixModeUpdater, {}, allowChmodToRead);
            }
            else {
                file = File(
                    fd.get(), [&](int fd) { return cacheFile(fd); },
                    capture_properties, unixModeUpdater, {}, allowChmodToRead);
            }
        }
        else {
            file = File(fd.get(), capture_properties, unixModeUpdater, {},
                        allowChmodToRead);
        }

        if (skipUpload) {
            LocalCasInstance::captureFileData(
                file.d_digest, fd.get(), root.empty() ? path : "",
                bypass_local_cache, move_file_hint);
        }
        else {
            captureFileData(file.d_digest, fd.get(), root.empty() ? path : "",
                            bypass_local_cache, move_file_hint);
        }

        const auto number_of_blobs_captured = static_cast<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue::Count>(
            1);

        buildboxcommon::buildboxcommonmetrics::CountingMetricUtil::
            recordCounterMetric(MetricNames::COUNTER_NUM_BLOBS_CAPTURED,
                                number_of_blobs_captured);
    }
    catch (const OutOfSpaceException &e) {
        google::rpc::Status status;
        const auto error_message =
            "Out of space error in `captureFile()` for path \"" + path +
            "\": " + e.what();
        logAndStoreMessage(grpc::StatusCode::RESOURCE_EXHAUSTED, error_message,
                           &status);
        *response.mutable_status() = std::move(status);
        return response;
    }
    catch (const std::runtime_error &e) {
        google::rpc::Status status;
        const auto error_message =
            std::string(e.what()) + " thrown for path: " + path;
        logAndStoreMessage(grpc::StatusCode::INTERNAL, error_message, &status);
        *response.mutable_status() = std::move(status);
        return response;
    }
    catch (const std::invalid_argument &e) {
        google::rpc::Status status;
        const auto error_message =
            "Invalid argument error in `captureFile()` for path \"" + path +
            "\": " + e.what();
        logAndStoreMessage(grpc::StatusCode::INTERNAL, error_message, &status);
        *response.mutable_status() = std::move(status);
        return response;
    }

    response.mutable_status()->set_code(grpc::StatusCode::OK);

    response.mutable_digest()->CopyFrom(file.d_digest);
    response.set_is_executable(file.d_executable);

    if (file.d_nodeProperties.ByteSizeLong() > 0) {
        response.mutable_node_properties()->CopyFrom(file.d_nodeProperties);
    }

    return response;
}

void LocalCasInstance::copyToLocalStorage(const Digest &digest, int orig_fd,
                                          int dirfd, const std::string &path)
{
    if (d_storage->hasBlob(digest)) {
        return;
    }

    FileDescriptor fd(orig_fd, false);
    if (fd.get() < 0) {
        fd = FileDescriptor(openat(dirfd, path.c_str(), O_RDONLY | O_CLOEXEC));
        if (fd.get() < 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Could not open file " << path);
        }
    }

    Digest computed_digest;
    d_storage->writeBlob(fd.get(), &computed_digest);

    if (digest != computed_digest) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument,
            "Digest " << digest << " does not match data (expected "
                      << computed_digest << ")");
    }
}

Digest LocalCasInstance::UploadAndStore(
    int dirfd, const buildboxcommon::digest_string_map &digest_blob_map,
    const buildboxcommon::digest_string_map &digest_path_map,
    const std::optional<Tree> &t, const bool, const bool move_files_hint)
{
    for (const auto &it : digest_blob_map) {
        const Digest &digest = it.first;
        d_storage->writeBlob(digest, it.second);
    }

    for (const auto &it : digest_path_map) {
        const Digest &digest = it.first;
        const std::string &path = it.second;
        addCapturedFileToLocalStorage(digest, -1, dirfd, path,
                                      move_files_hint);
    }

    Digest tree_digest;
    if (t.has_value()) {
        // Write the `Tree` message as well:
        tree_digest = buildboxcommon::DigestGenerator::hash(*t);
        d_storage->writeBlob(tree_digest, t->SerializeAsString());
    }

    return tree_digest;
}

grpc::Status LocalCasInstance::FetchTree(const FetchTreeRequest &request,
                                         FetchTreeResponse *)
{
    buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
        MetricNames::COUNTER_NAME_LOCAL_CAS_NUM_FETCH_TREE_REQUESTS, 1);
    if (treeIsAvailableLocally(request.root_digest(),
                               request.fetch_file_blobs())) {
        return grpc::Status::OK;
    }
    else {
        return grpc::Status(
            grpc::StatusCode::NOT_FOUND,
            "The tree is not available in the local CAS or incomplete.");
    }
}

grpc::Status LocalCasInstance::UploadTree(const UploadTreeRequest &request,
                                          UploadTreeResponse *)
{
    if (treeIsAvailableLocally(request.root_digest(), true)) {
        return grpc::Status::OK;
    }
    else {
        return grpc::Status(
            grpc::StatusCode::NOT_FOUND,
            "The tree is not available in the local CAS or incomplete.");
    }
}

buildboxcommon::FileStager *LocalCasInstance::getFileStager(
    StageTreeRequest_StagingMode mode,
    const std::optional<buildboxcommon::ProcessCredentials> &accessCredentials)
{
    if (!d_fileStagers.contains(mode)) {
        return nullptr;
    }

    const bool isDifferentUser =
        accessCredentials.has_value() && accessCredentials->uid != d_uid;

    if (isDifferentUser) {
        if (mode == StageTreeRequest_StagingMode_DEFAULT) {
            return d_fileStagers.at(StageTreeRequest_StagingMode_COPY_OR_LINK)
                .get();
        }
        if (mode == StageTreeRequest_StagingMode_FUSE) {
            // fuse-stager is not supported if the uid is different
            return nullptr;
        }
    }

    return d_fileStagers.at(mode).get();
}

void LocalCasInstance::setInodeCache(std::shared_ptr<InodeCache> inodeCache)
{
    d_inodeCache = std::move(inodeCache);
}
