Skip to content

File uploader.cpp

File List > device > uploader.cpp

Go to the documentation of this file

#include "uploader.h"
#include "logger.h"
#include "util/sha1.h"

#include <fstream>
#include <memory>
#include <string>
#include <filesystem>
#include <dirent.h>
#include <optional>

namespace jac {

class Sha1Hasher {
public:
    Sha1Hasher() {
        mbedtls_sha1_init(&_ctx);
    }
    Sha1Hasher(const Sha1Hasher&) = delete;
    ~Sha1Hasher() {
         mbedtls_sha1_free(&_ctx);
    }

    std::span<const uint8_t, 20> processFile(const std::filesystem::path& path) {
        _res.fill(0);

        auto file = std::fstream(path, std::ios::in | std::ios::binary);
        if (!file.is_open()) {
            return _res;
        }

        mbedtls_sha1_starts(&_ctx);

        uint8_t buf[256];
        size_t read = 0;
        do {
            file.read(reinterpret_cast<char*>(buf), sizeof(buf));;
            read = file.gcount();
            mbedtls_sha1_update(&_ctx, buf, read);
        } while(read > 0);

        mbedtls_sha1_finish(&_ctx, _res.data());
        return _res;
    }

private:
    std::array<uint8_t, 20> _res;
    mbedtls_sha1_context _ctx;
};

static std::optional<std::filesystem::path> getAbsolute(std::string filename, std::filesystem::path& rootDir) {
    std::filesystem::path normal = (rootDir / filename).lexically_normal();
    auto [it, _] = std::mismatch(rootDir.begin(), rootDir.end(), normal.begin(), normal.end());
    if (it == rootDir.end()) {
        return normal;
    }
    return std::nullopt;
}


static std::optional<std::pair<std::vector<std::string>, size_t>> listDir(std::filesystem::path& path) {
    size_t dataSize = 0;
    std::vector<std::string> files;
    DIR *dir;
    struct dirent *ent;
    dir = opendir(path.c_str());
    if (dir == NULL) {
        return std::nullopt;
    }
    while ((ent = readdir(dir)) != NULL) {
        try {
            files.push_back(ent->d_name);
        }
        catch (std::bad_alloc& e) {
            closedir(dir);
            return std::nullopt;
        }
        dataSize += files.back().size() + 1;
    }
    closedir(dir);
    return std::make_pair(files, dataSize);
}


static bool deleteDir(std::filesystem::path& path, bool onlyContents) {
    auto list = listDir(path);
    if (!list) {
        return false;
    }

    for (auto& file : list->first) {
        auto fullPath = path / file;
        if (std::filesystem::is_directory(fullPath)) {
            if (!deleteDir(fullPath, false)) {
                return false;
            }
        }
        else if (!std::filesystem::remove(fullPath)) {
            return false;
        }
    }

    if (onlyContents) {
        return true;
    }
    return std::filesystem::remove(path);
}

void Uploader::lockTimeout() {
    _state = State::NONE;
    _file.close();
    _onData = nullptr;
    _onDataComplete = nullptr;
}

bool Uploader::processPacket(int sender, std::span<const uint8_t> data) {
    if (!_devLock.ownedBy(sender)) {
        Logger::debug("Uploader: lock not owned by sender " + std::to_string(sender));
        auto response = this->_output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::LOCK_NOT_OWNED));
        response->send();
        return false;
    }

    if (data.size() < 1) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::ERROR));
        response->put(static_cast<uint8_t>(Error::UNKNOWN_COMMAND));
        response->send();
        return false;
    }
    auto begin = data.begin();
    Command cmd = static_cast<Command>(*begin);
    ++begin;

    if (_state == State::WAITING_FOR_DATA) {
        bool success = false;
        switch (cmd) {
            case Command::HAS_MORE_DATA:
                success = _onData(std::span<const uint8_t>(begin, data.end()));
                break;
            case Command::LAST_DATA:
                success = _onData(std::span<const uint8_t>(begin, data.end()));
                if (success) {
                    success = (!_onDataComplete) || _onDataComplete();
                    _state = State::NONE;
                    _file.close();
                    _onData = nullptr;
                    _onDataComplete = nullptr;
                }
                break;
            default:
                auto response = _output->buildPacket({sender});
                response->put(static_cast<uint8_t>(Command::ERROR));
                response->put(static_cast<uint8_t>(Error::UNKNOWN_COMMAND));
                response->put(static_cast<uint8_t>(cmd));
                response->send();
                break;
        }

        if (!success) {
            _state = State::NONE;
            _file.close();
            _onData = nullptr;
            _onDataComplete = nullptr;
        }
        return success;
    }

    switch (cmd) {
        case Command::READ_FILE:
            return processReadFile(sender, std::span<const uint8_t>(begin, data.end()));
        case Command::WRITE_FILE:
            return processWriteFile(sender, std::span<const uint8_t>(begin, data.end()));
        case Command::DELETE_FILE:
            return processDeleteFile(sender, std::span<const uint8_t>(begin, data.end()));
        case Command::LIST_DIR:
            return processListDir(sender, std::span<const uint8_t>(begin, data.end()));
        case Command::CREATE_DIR:
            return processCreateDir(sender, std::span<const uint8_t>(begin, data.end()));
        case Command::DELETE_DIR:
            return processDeleteDir(sender, std::span<const uint8_t>(begin, data.end()));
        case Command::FORMAT_STORAGE:
            return processFormatStorage(sender, std::span<const uint8_t>(begin, data.end()));
        case Command::GET_DIR_HASHES:
            return processGetHashes(sender, std::span<const uint8_t>(begin, data.end()));
        case Command::LIST_RESOURCES:
            return processListResources(sender, std::span<const uint8_t>(begin, data.end()));
        case Command::READ_RESOURCE:
            return processReadResource(sender, std::span<const uint8_t>(begin, data.end()));
        case Command::HAS_MORE_DATA:
        case Command::LAST_DATA:
        case Command::OK:
        case Command::ERROR:
        case Command::NOT_FOUND:
        case Command::CONTINUE:
        case Command::LOCK_NOT_OWNED:
            // these commands are invalid in this context
            break;
    }

    // unknown command
    auto response = _output->buildPacket({sender});
    response->put(static_cast<uint8_t>(Command::ERROR));
    response->put(static_cast<uint8_t>(Error::UNKNOWN_COMMAND));
    response->put(static_cast<uint8_t>(cmd));
    response->send();
    return false;
}

bool Uploader::processReadFile(int sender, std::span<const uint8_t> data) {
    auto begin = data.begin();
    std::string filename(begin, data.end());
    auto path = getAbsolute(filename, _rootDir);
    if (!path) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::NOT_FOUND));
        response->send();
        return false;
    }

    _file = std::fstream(*path, std::ios::in | std::ios::binary);
    if (!_file.is_open()) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::NOT_FOUND));
        response->send();
        return false;
    }

    std::vector<uint8_t> buff(_output->maxPacketSize({sender}) - 1);

    Command prefix = Command::HAS_MORE_DATA;
    size_t read = 1;
    while (read > 0) {
        _file.read(reinterpret_cast<char*>(buff.data()), buff.size());
        read = _file.gcount();

        if (read < buff.size()) {
            prefix = Command::LAST_DATA;
        }
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(prefix));
        response->put(std::span(buff.data(), read));
        response->send();
    }
    return true;
}

bool Uploader::processWriteFile(int sender, std::span<const uint8_t> data) {
    auto filenameEnd = std::find(data.begin(), data.end(), '\0');
    if (filenameEnd == data.end()) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::ERROR));
        response->put(static_cast<uint8_t>(Error::INVALID_FILENAME));
        response->send();
        return false;
    }

    std::string filename(data.begin(), filenameEnd);
    auto begin = ++filenameEnd;
    _state = State::WAITING_FOR_DATA;

    auto path = getAbsolute(filename, _rootDir);
    if (!path) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::NOT_FOUND));
        response->send();
        return false;
    }

    _file = std::fstream(*path, std::ios::out | std::ios::binary | std::ios::trunc);
    if (!_file.is_open()) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::ERROR));
        response->put(static_cast<uint8_t>(Error::FILE_OPEN_FAILED));
        response->send();
        return false;
    }
    _onData = [this, sender](std::span<const uint8_t> data_) {
        _file.write(reinterpret_cast<const char*>(data_.data()), data_.size());
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::CONTINUE));
        response->send();
        return true;
    };
    _onDataComplete = [this, sender]() {
        _file.sync();
        _file.close();
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::OK));
        response->send();
        return true;
    };

    if (begin != data.end()) {
        processPacket(sender, std::span<const uint8_t>(begin, data.end()));
    }

    return true;
}

bool Uploader::processDeleteFile(int sender, std::span<const uint8_t> data) {
    auto begin = data.begin();
    std::string filename(begin, data.end());
    auto path = getAbsolute(filename, _rootDir);
    if (!path) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::NOT_FOUND));
        response->send();
        return false;
    }

    bool success;

    try {
        success = !std::filesystem::is_directory(*path) && std::filesystem::remove(*path);
    }
    catch (const std::filesystem::filesystem_error& e) {
        Logger::error(std::string("Failed to delete file: ") + e.what());
        success = false;
    }

    if (success) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::OK));
        response->send();
        return true;
    }
    else {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::ERROR));
        response->put(static_cast<uint8_t>(Error::FILE_DELETE_FAILED));
        response->send();
        return false;
    }
}

bool Uploader::processListDir(int sender, std::span<const uint8_t> data) {
    auto dataIt = std::find(data.begin(), data.end(), '\0');
    std::string filename(data.begin(), dataIt);
    auto path = getAbsolute(filename, _rootDir);
    if (!path) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::NOT_FOUND));
        response->send();
        return false;
    }

    struct {
        bool directory = false;
        bool size = false;
    } flags;

    for (dataIt++; dataIt < data.end(); dataIt++) {
        switch (*dataIt) {
            case 'd':
                flags.directory = true;
                break;
            case 's':
                flags.size = true;
                break;
            default:
                break;
        }
    }

    bool isDir;
    try {
        isDir = std::filesystem::is_directory(*path);
    }
    catch (const std::filesystem::filesystem_error& e) {
        Logger::error(std::string("Failed to list directory: ") + e.what());
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::ERROR));
        response->put(static_cast<uint8_t>(Error::DIR_OPEN_FAILED));
        response->send();
        return false;
    }

    if (flags.directory || !isDir) {
        if (std::filesystem::exists(*path)) {
            std::string name = path->filename().string();
            auto response = _output->buildPacket({sender});
            response->put(static_cast<uint8_t>(Command::LAST_DATA));
            response->put(static_cast<uint8_t>(isDir ? 'd' : 'f'));
            response->put(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(name.data()), name.size()));
            response->put(static_cast<uint8_t>('\0'));
            response->send();
            return true;
        }
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::NOT_FOUND));
        response->send();
        return false;
    }

    std::vector<std::string> files;
    size_t dataSize = 0;
    // for (const auto& entry : std::filesystem::directory_iterator(path)) {
    //     files.push_back(entry.path().filename().string());
    //     dataSize += files.back().size() + 1;
    // }
    // XXX: std::filesystem::directory_iterator not working on esp-idf
    auto result = listDir(*path);

    if (!result) {
        Logger::error(std::string("Failed to list directory"));
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::ERROR));
        response->put(static_cast<uint8_t>(Error::DIR_OPEN_FAILED));
        response->send();
        return false;
    }

    std::tie(files, dataSize) = *result;
    dataSize += files.size() * 5; // for the type byte and size

    auto it = files.begin();
    Command prefix = Command::HAS_MORE_DATA;
    do {
        if (dataSize <= _output->maxPacketSize({sender}) - 1) {
            prefix = Command::LAST_DATA;
        }
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(prefix));
        while (it != files.end() && it->size() + 1 <= response->space()) {
            char type = 'f';
            uint32_t size = 0;
            try {
                type = std::filesystem::is_directory(*path / *it) ? 'd' : 'f';
            }
            catch (const std::filesystem::filesystem_error& e) {
                Logger::error(std::string("Failed to check file type: ") + e.what());
            }
            try {
                size = (flags.size && type == 'f') ? std::filesystem::file_size(*path / *it) : 0;
            }
            catch (const std::filesystem::filesystem_error& e) {
                Logger::error(std::string("Failed to get file size: ") + e.what());
            }

            response->put(static_cast<uint8_t>(type));
            response->put(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(it->data()), it->size()));
            response->put(static_cast<uint8_t>('\0'));
            for (int i = 3; i >= 0; i--) {
                response->put(static_cast<uint8_t>((size & (uint32_t(0xff) << (i * 8))) >> (i * 8)));
            }
            dataSize -= it->size() + 6;
            ++it;
        }
        response->send();
    } while (it != files.end());
    return true;
}

bool Uploader::processCreateDir(int sender, std::span<const uint8_t> data) {
    auto begin = data.begin();
    std::string filename(begin, data.end());
    auto path = getAbsolute(filename, _rootDir);
    if (!path) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::NOT_FOUND));
        response->send();
        return false;
    }

    bool success;

    try {
        if(std::filesystem::is_directory(*path)) {
            success = true;
        } else {
            success = std::filesystem::create_directory(*path);
        }
    }
    catch (const std::filesystem::filesystem_error& e) {
        Logger::error(std::string("Failed to create directory: ") + e.what());
        success = false;
    }

    if (success) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::OK));
        response->send();
        return true;
    }
    else {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::ERROR));
        response->put(static_cast<uint8_t>(Error::DIR_CREATE_FAILED));
        response->send();
        return false;
    }
}

bool Uploader::processDeleteDir(int sender, std::span<const uint8_t> data) {
    auto begin = data.begin();
    std::string filename(begin, data.end());
    auto path = getAbsolute(filename, _rootDir);
    if (!path) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::NOT_FOUND));
        response->send();
        return false;
    }

    bool success;

    try {
        success = deleteDir(*path, path->lexically_normal() == _rootDir);
        // XXX: std::filesystem::remove_all not working on esp-idf
    }
    catch (const std::filesystem::filesystem_error& e) {
        Logger::error(std::string("Failed to delete directory: ") + e.what());
        success = false;
    }

    if (success) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::OK));
        response->send();
        return true;
    }
    else {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::ERROR));
        response->put(static_cast<uint8_t>(Error::DIR_DELETE_FAILED));
        response->send();
        return false;
    }
}

bool Uploader::processFormatStorage(int sender, std::span<const uint8_t> data) {
    if (data.size() != 1 && data[0] != static_cast<uint8_t>(Command::OK)) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::ERROR));
        response->send();
        return false;
    }

    _formatFS(_rootDir);

    auto response = _output->buildPacket({sender});
    response->put(static_cast<uint8_t>(Command::OK));
    response->send();

    std::this_thread::sleep_for(std::chrono::milliseconds(200));
    std::exit(0);
}

bool Uploader::processGetHashes(int sender, std::span<const uint8_t> data) {
    auto dataIt = std::find(data.begin(), data.end(), '\0');
    std::string filename(data.begin(), dataIt);
    auto path = getAbsolute(filename, _rootDir);
    if (!path) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::NOT_FOUND));
        response->send();
        return false;
    }

    bool isDir = false;
    try {
        isDir = std::filesystem::is_directory(*path);
    }
    catch (const std::filesystem::filesystem_error& e) {
        Logger::error(std::string("Failed to list directory: ") + e.what());
    }

    if (!isDir) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::ERROR));
        response->put(static_cast<uint8_t>(Error::DIR_OPEN_FAILED));
        response->send();
        return false;
    }

    const size_t root_path_len = path->string().size() + 1;
    std::vector<std::string> files;
    std::vector<std::filesystem::path> dirs = { *path };

    Sha1Hasher hasher;

    while(!dirs.empty()) {
        std::filesystem::path cur_path = dirs.back();
        dirs.pop_back();
        const auto cur_path_str = cur_path.string();
        const auto rel_cur_path = cur_path_str.size() > root_path_len ? cur_path_str.substr(root_path_len) : std::string();

        DIR *dir = opendir(cur_path_str.c_str());
        if (dir == NULL) {
            continue;
        }

        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::HAS_MORE_DATA));

        bool hasAnyData = false;

        struct dirent *ent;
        while ((ent = readdir(dir)) != NULL) {
            try {
                if(ent->d_type == DT_DIR) {
                    dirs.push_back(cur_path / ent->d_name);
                    continue;
                } else if(ent->d_type != DT_REG) {
                    continue;
                }

                std::string file_path;
                if(!rel_cur_path.empty()) {
                    file_path = rel_cur_path + "/" +  ent->d_name;
                } else {
                    file_path = std::string(ent->d_name);
                }

                if(file_path.size() + 1 > response->space()) {
                    response->send();
                    response = _output->buildPacket({sender});
                    response->put(static_cast<uint8_t>(Command::HAS_MORE_DATA));
                }

                response->put(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(file_path.c_str()), file_path.size()));
                response->put(static_cast<uint8_t>('\0'));

                if(20 > response->space()) {
                    response->send();
                    response = _output->buildPacket({sender});
                    response->put(static_cast<uint8_t>(Command::HAS_MORE_DATA));
                }
                response->put(hasher.processFile(cur_path / ent->d_name));
                hasAnyData = true;
            }
            catch (std::bad_alloc& e) {
                closedir(dir);
                response = _output->buildPacket({sender});
                response->put(static_cast<uint8_t>(Command::ERROR));
                response->put(static_cast<uint8_t>(Error::FILE_OPEN_FAILED));
                response->send();
                return false;
            }
        }
        closedir(dir);

        if(hasAnyData) {
            response->send();
        }
    }

    auto response = _output->buildPacket({sender});
    response->put(static_cast<uint8_t>(Command::LAST_DATA));
    response->send();
    return true;
}

bool Uploader::processListResources(int sender, std::span<const uint8_t> data) {
    auto response = this->_output->buildPacket({sender});
    response->put(static_cast<uint8_t>(Command::LAST_DATA));

    std::string out;
    for (auto& [name, rdata] : _resources) {
        response->put(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(name.data()), name.size()));
        response->put(static_cast<uint8_t>('\0'));
        for (int i = 3; i >= 0; i--) {
            response->put(static_cast<uint8_t>((rdata.size() & (uint32_t(0xff) << (i * 8))) >> (i * 8)));
        }
    }

    response->send();

    return true;
}

bool Uploader::processReadResource(int sender, std::span<const uint8_t> data) {
    auto begin = data.begin();
    std::string resource(begin, data.end());

    auto it = _resources.find(resource);
    if (it == _resources.end()) {
        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(Command::NOT_FOUND));
        response->send();
        return false;
    }

    std::span<const uint8_t> dataSpan = it->second;

    size_t windowSize = _output->maxPacketSize({sender}) - 1;

    Command prefix = Command::HAS_MORE_DATA;
    size_t sent = std::max(dataSpan.size(), std::size_t(1));
    while (sent > 0) {
        if (dataSpan.size() <= windowSize) {
            prefix = Command::LAST_DATA;
        }

        auto response = _output->buildPacket({sender});
        response->put(static_cast<uint8_t>(prefix));
        sent = response->put(dataSpan);
        Logger::debug("Read " + std::to_string(sent) + " bytes");
        response->send();

        dataSpan = dataSpan.subspan(sent);
    }
    return true;
}

} // namespace jac