/*
 * Copyright 2020 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 <buildboxcommon_fslocalcas.h>
#include <buildboxcommon_metricnames.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_temporaryfile.h>
#include <buildboxcommonmetrics_countingmetricutil.h>
#include <buildboxcommonmetrics_gaugemetricutil.h>

#include <cstdint>
#include <dirent.h>
#include <fcntl.h>
#include <fstream>
#include <iostream>
#include <limits>
#include <optional>
#include <pthread.h>
#include <span>
#include <stdexcept>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#if __APPLE__
#define st_mtim st_mtimespec
#endif

using namespace buildboxcommon;
using buildboxcommon::buildboxcommonmetrics::GaugeMetricUtil;

const int FsLocalCas::s_HASH_PREFIX_LENGTH = 2;

FsLocalCas::FsLocalCas(const std::string &root_path,
                       const bool allow_external_file_moves,
                       const bool storeWritableObjects,
                       const bool trackDiskUsage)
    : d_storage_root(root_path + ((root_path.back() == '/') ? "" : "/") +
                     "cas"),
      d_objects_directory(d_storage_root + "/objects"),
      d_temp_directory(d_storage_root + "/tmp"),
      d_staging_directory(d_storage_root + "/staging"),
      d_disk_usage_metadata_filepath(d_storage_root + "/disk_usage"),
      d_external_file_moves_allowed(allow_external_file_moves),
      d_storeWritableObjects(storeWritableObjects), d_process_uid(getuid()),
      d_trackDiskUsage(trackDiskUsage),
      d_emptyDigest(buildboxcommon::DigestGenerator::hash("")),
      d_hashLength(d_emptyDigest.hash().size())
{
    BUILDBOX_LOG_INFO("Creating LocalCAS in " << d_storage_root);

    try {
        buildboxcommon::FileUtils::createDirectory(root_path.c_str());

        buildboxcommon::FileUtils::createDirectory(d_storage_root.c_str());

        buildboxcommon::FileUtils::createDirectory(
            d_objects_directory.c_str());

        d_objects_directory_st_dev =
            getStat(AT_FDCWD, d_objects_directory).st_dev;

        d_object_subdir_fd_store = std::make_unique<ObjectSubDirsFDStore>(
            FsLocalCas::s_HASH_PREFIX_LENGTH, d_objects_directory);

        buildboxcommon::FileUtils::createDirectory(d_temp_directory.c_str());

        buildboxcommon::FileUtils::createDirectory(
            d_staging_directory.c_str());

        if (d_external_file_moves_allowed) {
            BUILDBOX_LOG_INFO(
                "Allowing external file moves (only if owned by the user that "
                "runs this process and in the same FS as the CAS directory)");
        }

        // We pre-add the empty blob (some clients will optimize away 0-byte
        // uploads):
        this->writeBlob(d_emptyDigest, "");
    }
    catch (std::system_error &e) {
        BUILDBOX_LOG_ERROR("Could not create CAS directory structure in "
                           << d_storage_root << ", " << e.what());
        throw e;
    }

    if (d_trackDiskUsage) {
        // We check whether the disk usage is cached in a metadata file:
        d_disk_usage = getDiskUsageFromMetadataFile();
        if (d_disk_usage != -1) {
            BUILDBOX_LOG_TRACE("Using disk usage read from "
                               << d_disk_usage_metadata_filepath << ": "
                               << d_disk_usage << " bytes")
        }
        else {
            d_disk_usage = getDiskUsage(true);
        }
    }
    else {
        deleteDiskUsageMetadataFile();
        d_disk_usage = getDiskUsage();
    }

    GaugeMetricUtil::setGauge(CommonMetricNames::GAUGE_NAME_DISK_USAGE,
                              d_disk_usage);
}

FsLocalCas::~FsLocalCas()
{
    if (d_trackDiskUsage) {
        try {
            writeDiskUsageMetadataFile();
        }
        catch (const std::exception &e) BUILDBOX_LOG_ERROR(
            "Failed to write disk usage to " << d_disk_usage_metadata_filepath
                                             << ": " << e.what());
    }
}

bool FsLocalCas::hasBlob(const Digest &digest)
{
    const auto pathAt = filePathAt(digest);
    const int utime_result = utimensat(pathAt.d_directory_fd,
                                       pathAt.d_file_name.c_str(), nullptr, 0);

    if (utime_result != 0) {
        if (errno == ENOENT ||
            errno == EBADF) { // Top directory and/or file do not exist.
            return false;
        }
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Could not update timestamp for "
                                           << pathAt.d_file_name);
    }

    return true;
}

int FsLocalCas::hardLink(int source_dirfd, const std::string &source_path,
                         int destination_dirfd,
                         const std::string &destination_path)
{
    // link(oldpath, newpath)
    const int link_result =
        linkat(source_dirfd, source_path.c_str(), destination_dirfd,
               destination_path.c_str(), 0);

    if (link_result != 0 && errno != EEXIST) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Could not hard link temporary file " << source_path << " to "
                                                  << destination_path);
    }

    return link_result;
}

bool FsLocalCas::deleteFile(int dirfd, const std::string &path)
{
    if (unlinkat(dirfd, path.c_str(), 0) == 0) {
        return true;
    }

    if (errno == ENOENT) {
        return false;
    }
    else {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Could not delete file " << path);
    }
}

blob_time_type FsLocalCas::getMtime(const struct stat &s)
{
    // Use microseconds instead of nanoseconds as system_clock is
    // limited to microsecond resolution at least on macOS and even
    // on Linux the precision of file timestamps is limited by the
    // kernel timer frequency.
    const int NANOSECONDS_PER_MICROSECOND = 1000;
    return std::chrono::system_clock::from_time_t(s.st_mtim.tv_sec) +
           std::chrono::microseconds(s.st_mtim.tv_nsec /
                                     NANOSECONDS_PER_MICROSECOND);
}

blob_time_type FsLocalCas::getMtime(const std::string &path)
{
    const struct stat st = getStat(AT_FDCWD, path);
    return getMtime(st);
}

struct stat FsLocalCas::getStat(int dirfd, const std::string &path)
{
    struct stat st{};
    const int stat_status = fstatat(dirfd, path.c_str(), &st, 0);
    if (stat_status == 0) {
        return st;
    }

    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "stat() failed for \"" << path << "\"");
}

void FsLocalCas::writeDiskUsageMetadataFile()
{
    const int64_t disk_usage = getDiskUsage();
    constexpr mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
    buildboxcommon::FileUtils::writeFileAtomically(
        d_disk_usage_metadata_filepath, std::to_string(disk_usage), mode,
        d_temp_directory);

    BUILDBOX_LOG_TRACE("Wrote disk usage to " << d_disk_usage_metadata_filepath
                                              << ": " << disk_usage
                                              << " bytes")
}

void FsLocalCas::deleteDiskUsageMetadataFile()
{
    // (This method is called unconditionally during writes and deletions, so
    // we want to make sure that it only attempts to delete the file once.)
    if (d_disk_usage_metadata_file_deleted) {
        return;
    }

    deleteFile(AT_FDCWD, d_disk_usage_metadata_filepath);
    // (Does not fail if the file is not present.)

    d_disk_usage_metadata_file_deleted = true;
}

int64_t FsLocalCas::getDiskUsageFromMetadataFile() const
{
    if (!buildboxcommon::FileUtils::isRegularFile(
            d_disk_usage_metadata_filepath.c_str())) {
        BUILDBOX_LOG_TRACE(
            "Disk usage metadata file "
            << d_disk_usage_metadata_filepath
            << " does not exist. Will need to calculate size of the CAS.");
        return -1;
    }

    // Reading file:
    std::string contents;
    try {
        contents = buildboxcommon::FileUtils::getFileContents(
            d_disk_usage_metadata_filepath.c_str());
    }
    catch (const std::runtime_error &e) {
        BUILDBOX_LOG_ERROR("Error opening disk usage metadata file \""
                           << d_disk_usage_metadata_filepath
                           << "\": " << e.what());
        return -1;
    }

    // Parsing the value:
    try {
        const int64_t parsed_value = std::stoll(contents);
        if (parsed_value >= 0) {
            return parsed_value;
        }

        BUILDBOX_LOG_WARNING("The value in disk usage metadata file \""
                             << d_disk_usage_metadata_filepath
                             << "\" is negative: " << parsed_value);
    }
    catch (const std::exception &e) {
        BUILDBOX_LOG_WARNING("The value in disk usage metadata file \""
                             << d_disk_usage_metadata_filepath
                             << "\" could not be parsed: " << e.what());
    }

    return -1;
}

bool FsLocalCas::writeBlob(const Digest &digest, const std::string &data)
{
    if (hasBlob(digest)) { // No need to perform the write.
        return false;
    }

    validateBlobDigest(digest, data);

    // Creating a temp file and writing the blob data into it...
    buildboxcommon::TemporaryFile temp_file(createTemporaryFile());
    const std::string temp_filename(temp_file.name());

    std::ofstream file(temp_filename, std::fstream::binary);
    file << data;
    file.close();

    if (!file.good()) {
        const std::string error_message = "Failed writing to temporary file " +
                                          temp_filename + ": " +
                                          strerror(errno);
        BUILDBOX_LOG_ERROR(error_message);

        if (errno == ENOSPC) {
            BUILDBOXCOMMON_THROW_EXCEPTION(OutOfSpaceException, error_message);
        }
        else {
            BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error, error_message);
        }
    }

    return linkTemporaryFileToCas(digest, AT_FDCWD, temp_filename);
}

bool FsLocalCas::writeBlob(int fd, Digest *digest)
{

    // Creating a temp file and writing the blob data into it...
    buildboxcommon::TemporaryFile temp_file(createTemporaryFile());
    int dest_fd = temp_file.fd();
    Digest computedDigest;

    if (FileUtils::cloneFile(fd, dest_fd) == 0) {
        // Successfully via copy_file_range
        computedDigest = buildboxcommon::DigestGenerator::hash(dest_fd);
    }
    else {
        // Fallback to copy + hash in one go
        const size_t bufsize = 65536;

        std::array<char, bufsize> buffer{};
        int64_t total_bytes_read = 0;

        auto digest_context =
            buildboxcommon::DigestGenerator::createDigestContext();

        ssize_t bytes_read = 0;
        while ((bytes_read = pread(fd, buffer.data(), buffer.size(),
                                   total_bytes_read)) > 0) {

            std::span<const char> span_buffer(buffer.data(), bytes_read);
            digest_context.update(span_buffer.data(), span_buffer.size());
            ssize_t bytes_written = 0;
            while (bytes_written < bytes_read) {
                const auto subspan = span_buffer.subspan(bytes_written);
                const ssize_t res =
                    write(dest_fd, subspan.data(), subspan.size());
                if (res <= 0) {
                    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                        std::system_error, errno, std::system_category,
                        "Failed writing to temporary file "
                            << temp_file.name());
                }
                // check for overflow. check is also written to avoid overflow
                // should be the same logic as: res + bytes_written > SSIZE_MAX
                if (res > SSIZE_MAX - bytes_written ||
                    res > bytes_read - bytes_written) {
                    BUILDBOXCOMMON_THROW_EXCEPTION(
                        std::runtime_error,
                        "write() returned invalid value"
                        "res = "
                            << res << ", bytes_read = " << bytes_read
                            << ", bytes_written = " << bytes_written);
                }
                bytes_written += res;
            }
            total_bytes_read += bytes_read;
        }

        if (bytes_read < 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Error in read on file descriptor " << fd);
        }
        computedDigest = digest_context.finalizeDigest();
    }

    if (digest != nullptr) {
        *digest = computedDigest;
    }
    return linkTemporaryFileToCas(computedDigest, AT_FDCWD, temp_file.name());
}

bool FsLocalCas::moveBlobFromTemporaryFile(
    const Digest &digest, buildboxcommon::TemporaryFile tempFile)
{
    const auto &path = tempFile.strname();

    // Checking that path is a file inside this instance's temp directory:
    const bool path_in_temp_dir =
        (path.substr(0, d_temp_directory.length()) == d_temp_directory);
    if (!path_in_temp_dir) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::invalid_argument,
                                       "`path` is not in " + d_temp_directory);
    }

    return linkTemporaryFileToCas(digest, AT_FDCWD, path);
}

bool FsLocalCas::moveBlobFromTemporaryFile(
    const Digest &digest, const buildboxcommon::TemporaryDirectory &directory,
    const std::string &filename)
{
    // Checking that path is a file inside this instance's temp directory:
    const bool pathInTempDir =
        (directory.strname().substr(0, d_temp_directory.length()) ==
         d_temp_directory);
    if (!pathInTempDir) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::invalid_argument,
                                       "`path` is not in " + d_temp_directory);
    }

    bool ret = linkTemporaryFileToCas(digest, directory.fd(), filename);

    try {
        deleteFile(directory.fd(), filename);
    }
    catch (std::system_error &e) {
        BUILDBOX_LOG_ERROR("Could not remove file from temporary directory \""
                           << directory.strname() << "/" << filename
                           << "\": " << e.what());
    }

    return ret;
}

bool FsLocalCas::linkTemporaryFileToCas(const Digest &digest, int dirfd,
                                        const std::string &path)
{
    buildboxcommon::buildboxcommonmetrics::CountingMetricUtil::
        recordCounterMetric(
            CommonMetricNames::COUNTER_NUM_BLOBS_WRITTEN_TO_LOCAL, 1);

    buildboxcommon::buildboxcommonmetrics::CountingMetricUtil::
        recordCounterMetric(CommonMetricNames::COUNTER_NAME_BYTES_WRITE,
                            digest.size_bytes());

    // If a metadata file with the cached disk usage is present, its
    // value will be out of date after the first insertion, so we want to
    // delete it.
    if (digest.size_bytes() > 0) {
        // But we don't want to delete the file when inserting the empty blob
        // because that happens during the initialization of a new CAS
        // instance.
        deleteDiskUsageMetadataFile();
    }

    // Object files should be immutable by default. Deny write access also to
    // the owner to reduce the risk of accidental corruption, unless explicitly
    // required.
    mode_t mode = S_IRUSR | S_IRGRP | S_IROTH;
    if (d_storeWritableObjects) {
        mode |= S_IWUSR;
    }
    const int chmod_result = fchmodat(dirfd, path.c_str(), mode, 0);
    if (chmod_result != 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Could not chmod temporary file " << path);
    }

    // Getting the path where the blob should be stored in CAS.
    // If the parent subdirectory does not exist, it will be (atomically)
    // created:
    const std::string path_in_cas = filePath(digest, true);

    // Now we can make a hard link from the CAS directory to the
    // temporary file before it gets deleted. This has the effect of writing
    // the file atomically into the local CAS.
    // In the worst case, someone could beat us to placing another copy of
    // the blob in the CAS. But, because the contents will be the same, that is
    // not a problem.
    const int link_result = hardLink(dirfd, path, AT_FDCWD, path_in_cas);

    if (link_result == 0 && d_trackDiskUsage) {
        d_disk_usage += static_cast<int64_t>(digest.size_bytes());

        GaugeMetricUtil::setGauge(CommonMetricNames::GAUGE_NAME_DISK_USAGE,
                                  d_disk_usage);
    }

    // Returning whether we performed the write or didn't have to due to the
    // blob being present.
    return (link_result == 0);
}

std::unique_ptr<std::string>
FsLocalCas::readBlob(const Digest &digest, int64_t offset, size_t length)
{
    const int64_t data_size = digest.size_bytes();

    if (offset < 0 || offset > data_size) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::out_of_range,
                                       "The given interval is out of bounds");
    }

    int64_t bytes_to_read = 0;
    if (length == npos) {
        bytes_to_read = data_size - offset;
    }
    else {
        bytes_to_read = static_cast<int64_t>(length);
    }

    if (static_cast<uint64_t>(bytes_to_read) > SSIZE_MAX ||
        bytes_to_read > data_size - offset) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::out_of_range,
                                       "The given interval is out of bounds");
    }

    std::ifstream file = openFile(digest);
    if (!file.is_open()) {
        return nullptr;
    }

    // The file exists and is now open.
    // The range that we want to read is inside the data.

    // (Even when reading 0 bytes we want to make sure that the digest is
    // present.)
    if (bytes_to_read == 0) {
        buildboxcommon::buildboxcommonmetrics::CountingMetricUtil::
            recordCounterMetric(
                CommonMetricNames::COUNTER_NUM_BLOBS_READ_FROM_LOCAL, 1);
        return std::make_unique<std::string>("");
    }

    file.seekg(static_cast<std::streamoff>(offset), std::ios_base::beg);

    std::unique_ptr<char[]> buffer(new char[bytes_to_read]);

    if (file.good()) {
        file.read(buffer.get(), static_cast<std::streamsize>(bytes_to_read));
    }

    if (file.good()) {
        buildboxcommon::buildboxcommonmetrics::CountingMetricUtil::
            recordCounterMetric(
                CommonMetricNames::COUNTER_NUM_BLOBS_READ_FROM_LOCAL, 1);
        buildboxcommon::buildboxcommonmetrics::CountingMetricUtil::
            recordCounterMetric(CommonMetricNames::COUNTER_NAME_BYTES_READ,
                                bytes_to_read);

        return std::make_unique<std::string>(
            std::string(buffer.get(), bytes_to_read));
    }

    // Seek or read failed
    int streamerr = errno;

    struct stat st{};
    const auto pathAt = filePathAt(digest);
    if (fstatat(pathAt.d_directory_fd, pathAt.d_file_name.c_str(), &st, 0) ==
            0 &&
        st.st_size != data_size) {
        BUILDBOX_LOG_ERROR("Deleting object " << digest
                                              << " as file has incorrect size "
                                              << st.st_size);
    }
    else {
        BUILDBOX_LOG_ERROR("Deleting object " << digest
                                              << " as file is not readable: "
                                              << strerror(streamerr));
    }

    deleteFile(pathAt.d_directory_fd, pathAt.d_file_name.c_str());

    // Indicate to the caller that the blob is not available
    return nullptr;
}

std::unique_ptr<std::string> FsLocalCas::readBlob(const Digest &digest)
{
    return readBlob(digest, 0, LocalCas::npos);
}

bool FsLocalCas::copyBlob(const Digest &digest, int dirfd, const char *path,
                          mode_t mode, struct stat *destStat)
{
    const auto pathAt = filePathAt(digest);

    struct stat srcStat{};
    try {
        FileUtils::copyFile(pathAt.d_directory_fd, pathAt.d_file_name.c_str(),
                            dirfd, path, mode, &srcStat, destStat);
    }
    catch (const std::system_error &e) {
        if (e.code().value() == ENOENT) {
            // Blob not found should not throw an exception
            return false;
        }
        throw;
    }

    if (srcStat.st_size != digest.size_bytes()) {
        BUILDBOX_LOG_ERROR("Deleting object " << digest
                                              << " as file has incorrect size "
                                              << srcStat.st_size);
        unlinkat(pathAt.d_directory_fd, pathAt.d_file_name.c_str(), 0);
        unlinkat(dirfd, path, 0);

        return false;
    }

    return true;
}

std::string FsLocalCas::path(const Digest &digest)
{
    if (!hasBlob(digest)) {
        BUILDBOXCOMMON_THROW_EXCEPTION(BlobNotFoundException,
                                       toString(digest));
    }

    return filePath(digest, false);
}

std::string FsLocalCas::pathUnchecked(const Digest &digest)
{
    return filePath(digest, false);
}

FdNamePair FsLocalCas::pathAt(const Digest &digest)
{
    if (!hasBlob(digest)) {
        BUILDBOXCOMMON_THROW_EXCEPTION(BlobNotFoundException,
                                       toString(digest));
    }

    return filePathAt(digest);
}

FdNamePair FsLocalCas::pathAtUnchecked(const Digest &digest)
{
    return filePathAt(digest);
}

const std::string &FsLocalCas::path() { return d_storage_root; }

buildboxcommon::TemporaryDirectory FsLocalCas::createStagingDirectory()
{
    return buildboxcommon::TemporaryDirectory(d_staging_directory.c_str(),
                                              "cas-tmpdir");
}

buildboxcommon::TemporaryFile FsLocalCas::createTemporaryFile()
{
    constexpr mode_t mode = S_IRUSR | S_IWUSR;
    return buildboxcommon::TemporaryFile(d_temp_directory.c_str(),
                                         "cas-tmpfile", mode);
}

buildboxcommon::TemporaryDirectory FsLocalCas::createTemporaryDirectory()
{
    return buildboxcommon::TemporaryDirectory(d_temp_directory.c_str(),
                                              "cas-tmpdir");
}

std::string FsLocalCas::filePath(const Digest &digest,
                                 bool create_parent_directory) const
{
    const std::string &hash = digest.hash();
    const std::string directory_name = hash.substr(0, s_HASH_PREFIX_LENGTH);
    const std::string file_name = hash.substr(s_HASH_PREFIX_LENGTH);

    const std::string path_in_cas = d_objects_directory + "/" + directory_name;

    if (create_parent_directory) {
        buildboxcommon::FileUtils::createDirectory(path_in_cas.c_str());
    }

    return path_in_cas + "/" + file_name;
}

FdNamePair FsLocalCas::filePathAt(const Digest &digest) const
{
    const std::string &hash = digest.hash();
    const int directory_fd =
        d_object_subdir_fd_store->getSubDirFDFromHash(hash);
    const std::string file_name = hash.substr(s_HASH_PREFIX_LENGTH);

    return FdNamePair(directory_fd, file_name);
}

void FsLocalCas::validateDigests(const Digest &given_digest,
                                 const Digest &computed_digest)
{
    if (given_digest == computed_digest) {
        return;
    }

    BUILDBOXCOMMON_THROW_EXCEPTION(
        std::invalid_argument, "Digest " << given_digest
                                         << " does not match data (expected "
                                         << computed_digest << ")");
}

void FsLocalCas::validateBlobDigest(const Digest &digest,
                                    const std::string &data) const
{
    const Digest computed_digest = buildboxcommon::DigestGenerator::hash(data);
    validateDigests(digest, computed_digest);
}

std::ifstream FsLocalCas::openFile(const Digest &digest)
{
    const std::string path = filePath(digest);

    std::ifstream file;
    file.open(path, std::fstream::binary);

    if (file.fail()) {
        const auto error_code = errno;
        if (error_code == ENOENT) {
            BUILDBOX_LOG_TRACE(
                "Attempted to open file not present in CAS: " << path);
            return std::ifstream(); // Reports `is_open() == false`
        }

        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(std::system_error, errno,
                                              std::system_category,
                                              "Could not open file " << path);
    }

    // Update timestamp
    hasBlob(digest);

    return file;
}

bool FsLocalCas::deleteBlob(const Digest &digest)
{
    if (digest.size_bytes() == 0) {
        // Because some clients do not upload blobs of size 0, we make sure
        // that the empty blob is always available.
        return false;
    }

    deleteDiskUsageMetadataFile();

    const std::string path = filePath(digest);

    if (deleteFile(AT_FDCWD, path)) {
        if (d_trackDiskUsage) {
            d_disk_usage -= static_cast<int64_t>(digest.size_bytes());

            GaugeMetricUtil::setGauge(CommonMetricNames::GAUGE_NAME_DISK_USAGE,
                                      d_disk_usage);
        }

        return true;
    }
    else {
        return false;
    }
}

bool FsLocalCas::deleteBlob(const Digest &digest,
                            const blob_time_type &if_unmodified_since)
{
    const std::string path = filePath(digest);
    const blob_time_type blob_mtime = getMtime(path);

    if (blob_mtime > if_unmodified_since) {
        return false;
    }

    return deleteBlob(digest);
}

static void logUnexpectedObject(const std::string &path)
{
    BUILDBOX_LOG_ERROR("Unexpected entry in objects directory: " + path);
}

bool FsLocalCas::digestFromFile(const std::string &dirname,
                                const std::string &filename, Digest *digest,
                                struct stat *st) const
{
    const int directory_fd =
        d_object_subdir_fd_store->getSubDirFDFromHash(dirname);

    if (fstatat(directory_fd, filename.c_str(), st, 0) != 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "error in fstatat() for object \"" << dirname << "/" << filename
                                               << "\"");
    }

    if (!S_ISREG(st->st_mode)) {
        return false;
    }

    const std::string hash = dirname + filename;
    // Basic hash validation
    if (hash.size() != d_hashLength) {
        return false;
    }

    digest->set_hash(hash);
    digest->set_size_bytes(st->st_size);

    return true;
}

class DirectoryIterator {
  public:
    DirectoryIterator(const std::string &path)
        : d_dir_stream(opendir(path.c_str()), closedir)
    {
        if (d_dir_stream == nullptr) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "error in opendir() for path \"" << path << "\"");
        }
    }

    struct dirent *next()
    {
        while (true) {
            auto entry = readdir(d_dir_stream.get());
            if (entry != nullptr && (strcmp(entry->d_name, ".") == 0 ||
                                     strcmp(entry->d_name, "..") == 0)) {
                // Skip "." and ".."
                continue;
            }
            return entry;
        }
    }

  private:
    std::unique_ptr<DIR, int (*)(DIR *)> d_dir_stream;
};

void FsLocalCas::listBlobs(digest_time_callback_type callback)
{
    auto dirIt = DirectoryIterator(d_objects_directory);
    for (auto entry = dirIt.next(); entry != nullptr; entry = dirIt.next()) {
        std::string subdirPath =
            std::string(d_objects_directory) + "/" + entry->d_name;

        // The objects directory should only contain hash prefix
        // subdirectories.
        if (strlen(entry->d_name) != s_HASH_PREFIX_LENGTH) {
            logUnexpectedObject(subdirPath);
            continue;
        }

        auto subdirIt = DirectoryIterator(subdirPath);
        for (auto objEntry = subdirIt.next(); objEntry != nullptr;
             objEntry = subdirIt.next()) {
            Digest digest;
            struct stat st{};

            if (!digestFromFile(entry->d_name, objEntry->d_name, &digest,
                                &st)) {
                logUnexpectedObject(subdirPath + "/" + objEntry->d_name);
                continue;
            }
            else if (st.st_size == 0 && digest != d_emptyDigest) {
                // On some filesystems, system crash or power loss shortly
                // after writing a file may result in an empty file even with
                // our atomic file write using a hardlink (the atomicity
                // guarantee holds only while the system is running).
                const auto path = subdirPath + "/" + objEntry->d_name;
                BUILDBOX_LOG_ERROR("Deleting empty object: " + path);
                deleteFile(AT_FDCWD, path);
                continue;
            }

            callback(digest, getMtime(st));
        }
    }
}

int64_t FsLocalCas::getAvailableDiskSpace()
{
    struct statvfs buf{};
    if (statvfs(d_storage_root.c_str(), &buf) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to get available disk space");
    }
    return static_cast<int64_t>(buf.f_bavail) *
           static_cast<int64_t>(buf.f_bsize);
}

bool FsLocalCas::moveBlobFromExternalFile(const Digest &digest, int dirfd,
                                          const std::string &path)
{
    if (!d_external_file_moves_allowed) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument,
            "Moving from external files is not allowed for this instance.");
    }

    // Verifying that the file is owned by the user running this process:
    const struct stat file_stat = getStat(dirfd, path);

    if (file_stat.st_uid != d_process_uid) {
        BUILDBOX_LOG_DEBUG("Cannot move ["
                           << path << "] to CAS: owner (" << file_stat.st_uid
                           << ") != process UID (" << d_process_uid << ")");
        return false;
    }

    if (file_stat.st_dev != d_objects_directory_st_dev) {
        BUILDBOX_LOG_DEBUG("Cannot move ["
                           << path << "] to CAS: device ID ("
                           << file_stat.st_dev << ") != object directory ("
                           << d_objects_directory << ") device ID ("
                           << d_objects_directory_st_dev << ")");
        return false;
    }

    linkTemporaryFileToCas(digest, dirfd, path);

    try {
        deleteFile(dirfd, path);
    }
    catch (std::system_error &e) {
        BUILDBOX_LOG_ERROR("Could not remove file \"" << path
                                                      << "\": " << e.what());
    }

    return true;
}

int64_t FsLocalCas::getDiskUsage(bool force_scan)
{
    if (!d_trackDiskUsage) {
        struct statvfs buf{};
        if (statvfs(d_storage_root.c_str(), &buf) < 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(std::system_error, errno,
                                                  std::system_category,
                                                  "Failed to get disk usage");
        }
        return static_cast<int64_t>(buf.f_blocks) *
                   static_cast<int64_t>(buf.f_frsize) -
               static_cast<int64_t>(buf.f_bavail) *
                   static_cast<int64_t>(buf.f_bsize);
    }
    else if (force_scan) {
        int64_t disk_usage = 0;

        listBlobs([&disk_usage](const buildboxcommon::Digest &digest,
                                blob_time_type) {
            disk_usage += static_cast<int64_t>(digest.size_bytes());
        });

        GaugeMetricUtil::setGauge(CommonMetricNames::GAUGE_NAME_DISK_USAGE,
                                  d_disk_usage);
        return disk_usage;
    }

    return d_disk_usage;
}

std::optional<int64_t> FsLocalCas::getDiskQuota()
{
    // This means no quota.
    return std::nullopt;
}
