// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved. // For licensing, see https://github.com/mudita/MuditaOS/blob/master/LICENSE.md #include #include #include #include namespace sdesktop::endpoints { namespace { constexpr auto bytesInMebibyte = 1024LLU * 1024LLU; enum FileType { directory, regularFile, symlink, other, }; auto parseFileEntry(const std::filesystem::directory_entry &entry) -> json11::Json { uintmax_t size = 0; FileType type; if (entry.is_directory()) { type = FileType::directory; } else { size = entry.file_size(); } if (entry.is_regular_file()) { type = FileType::regularFile; } else if (entry.is_symlink()) { type = FileType::symlink; } else { type = FileType::other; } return json11::Json::object{ {json::fs::path, entry.path().string()}, {json::fs::fileSize, size}, {json::fs::type, type}}; } } // namespace auto FS_Helper::processGet(Context &context) -> ProcessResult { const auto &body = context.getBody(); ResponseContext response{}; if (body[json::fs::fileName].is_string()) { response = startGetFile(context); } else if (body[json::fs::rxID].is_number()) { response = getFileChunk(context); } else if (body[json::fs::listDir].is_string()) { const auto &dir = body[json::fs::listDir].string_value(); response = requestListDir(dir); } else { LOG_ERROR("Unknown request"); response = {.status = http::Code::BadRequest}; } return {Sent::No, std::move(response)}; } auto FS_Helper::processPut(Context &context) -> ProcessResult { const auto &body = context.getBody(); auto code = http::Code::BadRequest; ResponseContext response{}; if (body[json::fs::fileName].is_string() && body[json::fs::fileSize].is_number() && body[json::fs::fileCrc32].is_string()) { response = startSendFile(context); } else if (body[json::fs::txID].is_number() && body[json::fs::chunkNo].is_number() && body[json::fs::data].is_string()) { response = sendFileChunk(context); } else if (body[json::fs::renameFile].is_string() && body[json::fs::destFilename].is_string()) { const auto &fileName = body[json::fs::renameFile].string_value(); const auto &destFilename = body[json::fs::destFilename].string_value(); code = requestFileRename(fileName, destFilename) ? http::Code::NoContent : http::Code::NotFound; response = ResponseContext{.status = code}; } else { LOG_ERROR("Bad request, missing or invalid argument"); response = ResponseContext{.status = http::Code::BadRequest}; } return {Sent::No, std::move(response)}; } auto FS_Helper::processDelete(Context &context) -> ProcessResult { const auto &body = context.getBody(); auto code = http::Code::BadRequest; if (body[json::fs::removeFile].is_string()) { const auto &fileName = body[json::fs::removeFile].string_value(); try { code = requestFileRemoval(fileName) ? http::Code::NoContent : http::Code::NotFound; } catch (const std::filesystem::filesystem_error &ex) { LOG_ERROR("Can't remove requested file %s, error: %d", fileName.c_str(), ex.code().value()); code = http::Code::InternalServerError; } } else { LOG_ERROR("Bad request, missing or invalid argument"); } return {Sent::No, ResponseContext{.status = code}}; } auto FS_Helper::requestLogsFlush() const -> void { auto ownerService = dynamic_cast(owner); if (ownerService != nullptr) { ownerService->requestLogsFlush(); } } auto FS_Helper::startGetFile(Context &context) const -> ResponseContext { const std::filesystem::path filePath = context.getBody()[json::fs::fileName].string_value(); try { requestLogsFlush(); } catch (const std::runtime_error &e) { LOG_ERROR("Logs flush exception: %s", e.what()); json11::Json::object response({{json::common::reason, e.what()}}); return ResponseContext{.status = http::Code::InternalServerError, .body = response}; } if (!std::filesystem::exists(filePath)) { LOG_ERROR("File %s not found", filePath.c_str()); json11::Json::object response({{json::common::reason, json::fs::fileDoesNotExist}}); return ResponseContext{.status = http::Code::NotFound, .body = response}; } json11::Json::object response{}; auto code = http::Code::BadRequest; LOG_DEBUG("Checking file"); try { auto [rxID, fileSize] = fileOps.createReceiveIDForFile(filePath); code = http::Code::OK; response = json11::Json::object({{json::fs::rxID, static_cast(rxID)}, {json::fs::fileSize, static_cast(fileSize)}, {json::fs::chunkSize, static_cast(FileOperations::ChunkSize)}}); } catch (std::runtime_error &e) { LOG_ERROR("FileOperations exception: %s", e.what()); code = http::Code::InternalServerError; response = json11::Json::object({{json::common::reason, std::string(e.what())}}); } catch (std::exception &e) { LOG_ERROR("FileOperations exception: %s", e.what()); code = http::Code::BadRequest; response = json11::Json::object({{json::common::reason, std::string(e.what())}}); } return ResponseContext{.status = code, .body = response}; } auto FS_Helper::getFileChunk(Context &context) const -> ResponseContext { const auto rxID = context.getBody()[json::fs::rxID].int_value(); const auto chunkNo = context.getBody()[json::fs::chunkNo].int_value(); FileOperations::DataWithCrc32 dataWithCrc32; try { dataWithCrc32 = fileOps.getDataForReceiveID(rxID, chunkNo); } catch (std::exception &e) { LOG_ERROR("Exception during getting data: %s", e.what()); json11::Json::object response({{json::common::reason, e.what()}}); return ResponseContext{.status = http::Code::BadRequest, .body = response}; } json11::Json::object response{}; auto code = http::Code::BadRequest; if (!dataWithCrc32.data.empty()) { code = http::Code::OK; response = json11::Json::object({{json::fs::rxID, static_cast(rxID)}, {json::fs::chunkNo, static_cast(chunkNo)}, {json::fs::data, dataWithCrc32.data}}); if (!dataWithCrc32.crc32.empty()) { response[json::fs::fileCrc32] = dataWithCrc32.crc32; } } else { std::ostringstream errorReason; errorReason << "Invalid request rxID: " << std::to_string(rxID) << ", chunkNo: " << std::to_string(chunkNo); LOG_ERROR("%s", errorReason.str().c_str()); code = http::Code::BadRequest; response = json11::Json::object({{json::common::reason, errorReason.str()}}); } return ResponseContext{.status = code, .body = response}; } auto FS_Helper::startSendFile(Context &context) const -> ResponseContext { const auto &body = context.getBody(); const auto &filePath = body[json::fs::fileName].string_value(); const std::uint32_t fileSize = body[json::fs::fileSize].int_value(); const auto &fileCrc32 = body[json::fs::fileCrc32].string_value(); auto code = http::Code::BadRequest; LOG_DEBUG("Start sending of file: %s", filePath.c_str()); if (fileSize == 0 || fileCrc32.empty()) { LOG_ERROR("File '%s' corrupted", filePath.c_str()); return ResponseContext{.status = code}; } const auto freeSpaceLeftForUserFilesMiB = getFreeSpaceForUserFilesMiB(); if ((freeSpaceLeftForUserFilesMiB - (static_cast(fileSize) / bytesInMebibyte)) <= 0) { LOG_ERROR("Not enough space left on device!"); code = http::Code::InsufficientStorage; return ResponseContext{.status = code}; } if (!std::filesystem::exists(filePath)) { LOG_DEBUG("Creating file %s", filePath.c_str()); } else { LOG_DEBUG("Overwriting file %s", filePath.c_str()); } json11::Json::object response{}; try { auto txID = fileOps.createTransmitIDForFile(filePath, fileSize, fileCrc32); code = http::Code::OK; response = json11::Json::object({{json::fs::txID, static_cast(txID)}, {json::fs::chunkSize, static_cast(FileOperations::ChunkSize)}}); } catch (std::runtime_error &e) { LOG_ERROR("FileOperations exception: %s", e.what()); code = http::Code::InternalServerError; response = json11::Json::object({{json::common::reason, std::string(e.what())}}); } catch (std::exception &e) { LOG_ERROR("FileOperations exception: %s", e.what()); code = http::Code::BadRequest; response = json11::Json::object({{json::common::reason, std::string(e.what())}}); } return ResponseContext{.status = code, .body = response}; } auto FS_Helper::sendFileChunk(Context &context) const -> ResponseContext { const auto &body = context.getBody(); const auto txID = body[json::fs::txID].int_value(); const auto chunkNo = body[json::fs::chunkNo].int_value(); const auto &data = body[json::fs::data].string_value(); if (data.empty()) { std::ostringstream errorReason; errorReason << "Invalid request txID: " << std::to_string(txID) << ", chunkNo: " << std::to_string(chunkNo); LOG_ERROR("%s", errorReason.str().c_str()); auto code = http::Code::BadRequest; auto response = json11::Json::object({{json::common::reason, errorReason.str()}}); return ResponseContext{.status = code, .body = response}; } auto returnCode = sys::ReturnCodes::Success; try { returnCode = fileOps.sendDataForTransmitID(txID, chunkNo, data); } catch (std::exception &e) { LOG_ERROR("Exception during sending data: %s", e.what()); auto code = http::Code::NotAcceptable; auto response = json11::Json::object({{json::common::reason, e.what()}}); return ResponseContext{.status = code, .body = response}; } json11::Json::object response{}; auto code = http::Code::OK; if (returnCode == sys::ReturnCodes::Success) { response = json11::Json::object( {{json::fs::txID, static_cast(txID)}, {json::fs::chunkNo, static_cast(chunkNo)}}); } else { code = http::Code::BadRequest; response = json11::Json::object( {{json::fs::txID, static_cast(txID)}, {json::fs::chunkNo, static_cast(chunkNo)}}); } return ResponseContext{.status = code, .body = response}; } auto FS_Helper::requestFileRemoval(const std::string &fileName) -> bool { return std::filesystem::remove(fileName); } auto FS_Helper::requestFileRename(const std::string &fileName, const std::string &destFilename) noexcept -> bool { std::error_code ec; std::filesystem::rename(fileName, destFilename, ec); if (!ec) { LOG_ERROR("Failed to rename %s, error: %d", fileName.c_str(), ec.value()); } return !ec; } auto FS_Helper::requestListDir(const std::string &directory) -> ResponseContext { if (!std::filesystem::exists(directory)) { return ResponseContext{.status = http::Code::NotFound}; } json11::Json::array jsonArr; for (const auto &entry : std::filesystem::directory_iterator{directory}) { jsonArr.push_back(parseFileEntry(entry)); } json11::Json::object const response({{directory, jsonArr}}); return ResponseContext{.status = http::Code::OK, .body = response}; } auto FS_Helper::getFreeSpaceForUserFilesMiB() const -> float { const auto userDiskPath = purefs::dir::getUserDiskPath(); struct statvfs vfstat {}; if (statvfs(userDiskPath.c_str(), &vfstat) < 0) { return 0; } const auto freeBytes = (static_cast(vfstat.f_bfree) * static_cast(vfstat.f_bsize)); const auto freeMiBs = freeBytes / bytesInMebibyte; return freeMiBs; } } // namespace sdesktop::endpoints