diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index 2ed6e20..6bc1a4f 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -21,9 +21,11 @@ add_executable(backend src/controllers/controllers.h src/controllers/admin.cpp - src/controllers/fs.cpp src/controllers/user.cpp + src/controllers/fs/fs_routes.cpp + src/controllers/fs/fs_functions.cpp + src/controllers/auth/auth_common.cpp src/controllers/auth/auth_basic.cpp src/controllers/auth/auth_2fa.cpp diff --git a/backend/src/controllers/admin.cpp b/backend/src/controllers/admin.cpp index 87dbcbf..9de8720 100644 --- a/backend/src/controllers/admin.cpp +++ b/backend/src/controllers/admin.cpp @@ -62,6 +62,9 @@ namespace api { db::MapperUser user_mapper(drogon::app().getDbClient()); auto user = user_mapper.findByPrimaryKey(user_id); auth::revoke_all(user); + + std::unique_lock lock(*fs::get_user_mutex(user.getValueOfId())); + fs::delete_node(fs::get_node(user.getValueOfRootId()).value(), chan, true); user_mapper.deleteOne(user); cbk(dto::Responses::get_success_res()); diff --git a/backend/src/controllers/controllers.h b/backend/src/controllers/controllers.h index d91a9ed..0e630aa 100644 --- a/backend/src/controllers/controllers.h +++ b/backend/src/controllers/controllers.h @@ -1,10 +1,14 @@ #ifndef BACKEND_CONTROLLERS_H #define BACKEND_CONTROLLERS_H #include +#include +#include #include #include #include +#include +#include #include "db/db.h" @@ -111,7 +115,7 @@ public: static std::variant> create_node(std::string name, const db::User& owner, bool file, const std::optional &parent, bool force = false); static void delete_node(db::INode node, msd::channel& chan, bool allow_root = false); - + static std::shared_ptr get_user_mutex(uint64_t user_id); void root(req_type, cbk_type); void node(req_type, cbk_type, uint64_t node); @@ -124,6 +128,18 @@ public: void download_multi(req_type, cbk_type); void download_preview(req_type, cbk_type, uint64_t node); void get_type(req_type, cbk_type, uint64_t node); + +private: + static trantor::EventLoop* get_zip_loop(); + static trantor::EventLoop* get_delete_loop(); + static void generate_path(db::INode node, std::string& str); + static Json::Value generate_path(db::INode node); + static uint64_t calc_total_size(const db::INode& base); + static void add_to_zip(struct zip_t* zip, const std::string& key, const db::INode& node, const std::string& path); + + static uint64_t next_temp_id; + static std::unordered_map zip_to_temp_map; + static std::unordered_map> in_progress_zips; }; class user : public drogon::HttpController { diff --git a/backend/src/controllers/fs/fs_functions.cpp b/backend/src/controllers/fs/fs_functions.cpp new file mode 100644 index 0000000..ba707bf --- /dev/null +++ b/backend/src/controllers/fs/fs_functions.cpp @@ -0,0 +1,257 @@ +#include +#include + +#include "controllers/controllers.h" +#include "dto/dto.h" + +char windows_invalid_chars[] = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F<>:\"/\\|"; + +namespace api { + uint64_t fs::next_temp_id = 0; + std::unordered_map fs::zip_to_temp_map; + std::unordered_map> fs::in_progress_zips; + + std::optional fs::get_node(uint64_t node) { + db::MapperInode inode_mapper(drogon::app().getDbClient()); + try { + return inode_mapper.findByPrimaryKey(node); + } catch (const std::exception&) { + return std::nullopt; + } + } + + std::optional fs::get_node_and_validate(const db::User &user, uint64_t node) { + auto inode = get_node(node); + if (!inode.has_value()) return std::nullopt; + if (inode->getValueOfOwnerId() != user.getValueOfId()) return std::nullopt; + return inode; + } + + std::vector fs::get_children(const db::INode& parent) { + db::MapperInode inode_mapper(drogon::app().getDbClient()); + return inode_mapper.findBy(db::Criteria(db::INode::Cols::_parent_id, db::CompareOps::EQ, parent.getValueOfId())); + } + + std::variant> + fs::create_node(std::string name, const db::User& owner, bool file, const std::optional &parent, bool force) { + // Stolen from https://github.com/boostorg/filesystem/blob/develop/src/portability.cpp + if (!force) + if (name.empty() || name[0] == ' ' || name.find_first_of(windows_invalid_chars, 0, sizeof(windows_invalid_chars)) != std::string::npos || *(name.end() - 1) == ' ' || *(name.end() - 1) == '.' || name == "." || name == "..") + return {create_node_error::INVALID_NAME}; + + db::INode node; + node.setIsFile(file ? 1 : 0); + node.setName(name); + node.setOwnerId(owner.getValueOfId()); + node.setHasPreview(0); + if (parent.has_value()) { + auto parent_node = get_node_and_validate(owner, *parent); + if (!parent_node.has_value()) + return {create_node_error::INVALID_PARENT}; + if (parent_node->getValueOfIsFile() != 0) + return {create_node_error::FILE_PARENT}; + auto children = get_children(*parent_node); + for (const auto& child : children) + if (child.getValueOfName() == name) + return {std::make_tuple( + child.getValueOfIsFile() != 0, + child.getValueOfId() + )}; + node.setParentId(*parent); + } + db::MapperInode inode_mapper(drogon::app().getDbClient()); + inode_mapper.insert(node); + return {node}; + } + + void fs::delete_node(db::INode node, msd::channel& chan, bool allow_root) { + if (node.getValueOfParentId() == 0 && (!allow_root)) return; + + db::MapperInode inode_mapper(drogon::app().getDbClient()); + + const auto delete_file = [&chan, &inode_mapper](const db::INode& node) { + std::string entry = "Deleting "; + generate_path(node, entry); + entry >> chan; + std::filesystem::path p("./files"); + p /= std::to_string(node.getValueOfId()); + std::filesystem::remove(p); + if (node.getValueOfHasPreview() != 0) + std::filesystem::remove(p.string() + "_preview.png"); + inode_mapper.deleteOne(node); + std::string(" Done\n") >> chan; + }; + + std::stack queue, files, folders; + + if (node.getValueOfIsFile() == 0) queue.push(node); + else files.push(node); + + while (!queue.empty()) { + while (!files.empty()) { + delete_file(files.top()); + files.pop(); + } + std::string entry = "Deleting "; + generate_path(queue.top(), entry); + entry += "\n"; + entry >> chan; + auto children = get_children(queue.top()); + folders.push(queue.top()); + queue.pop(); + for (const auto& child : children) { + if (child.getValueOfIsFile() == 0) queue.push(child); + else files.push(child); + } + } + + while (!files.empty()) { + delete_file(files.top()); + files.pop(); + } + + while (!folders.empty()) { + inode_mapper.deleteOne(folders.top()); + folders.pop(); + } + } + + std::shared_ptr fs::get_user_mutex(uint64_t user_id) { + static std::unordered_map> mutexes; + static std::mutex mutexes_mutex; + std::lock_guard guard(mutexes_mutex); + return (*mutexes.try_emplace(user_id, std::make_shared()).first).second; + } + + trantor::EventLoop* fs::get_zip_loop() { + static bool init_done = false; + static trantor::EventLoopThread loop("ZipEventLoop"); + if (!init_done) { + init_done = true; + loop.run(); + loop.getLoop()->runEvery(30*60, []{ + for (const auto& entry : std::filesystem::directory_iterator("./temp")) { + if (!entry.is_regular_file()) continue; + const std::string file_name = "./temp/" + entry.path().filename().string(); + const auto& progress_pos = std::find_if(in_progress_zips.begin(), in_progress_zips.end(), + [&file_name](const std::pair>& entry) { + return std::get<0>(entry.second) == file_name; + } + ); + if (progress_pos != in_progress_zips.end()) return; + const auto& zip_map_pos = std::find_if(zip_to_temp_map.begin(), zip_to_temp_map.end(), + [&file_name](const std::pair& entry){ + return entry.second == file_name; + } + ); + if (zip_map_pos != zip_to_temp_map.end()) return; + std::filesystem::remove(entry.path()); + } + }); + } + return loop.getLoop(); + } + + trantor::EventLoop* fs::get_delete_loop() { + static bool init_done = false; + static trantor::EventLoopThread loop("DeleteEventLoop"); + if (!init_done) { + init_done = true; + loop.run(); + } + return loop.getLoop(); + } + + void fs::generate_path(db::INode node, std::string& str) { + db::MapperInode inode_mapper(drogon::app().getDbClient()); + std::stack path; + path.push(node); + while (node.getParentId() != nullptr) { + node = inode_mapper.findByPrimaryKey(node.getValueOfParentId()); + path.push(node); + } + while (!path.empty()) { + const db::INode& seg = path.top(); + str += seg.getValueOfName(); + if (seg.getValueOfIsFile() == 0) str += "/"; + path.pop(); + } + } + + Json::Value fs::generate_path(db::INode node) { + Json::Value segments = Json::Value(Json::ValueType::arrayValue); + db::MapperInode inode_mapper(drogon::app().getDbClient()); + std::stack path; + path.push(node); + while (node.getParentId() != nullptr) { + node = inode_mapper.findByPrimaryKey(node.getValueOfParentId()); + path.push(node); + } + while (!path.empty()) { + const db::INode& seg = path.top(); + if (seg.getParentId() == nullptr) { + Json::Value json_seg; + json_seg["path"] = "/"; + json_seg["node"] = seg.getValueOfId(); + segments.append(json_seg); + } else { + Json::Value json_seg; + json_seg["path"] = seg.getValueOfName(); + json_seg["node"] = seg.getValueOfId(); + segments.append(json_seg); + if (seg.getValueOfIsFile() == 0) { + json_seg.removeMember("node"); + json_seg["path"] = "/"; + segments.append(json_seg); + } + } + path.pop(); + } + Json::Value resp; + resp["segments"] = segments; + return resp; + } + + uint64_t fs::calc_total_size(const db::INode& base) { + uint64_t size = 0; + std::stack queue; + queue.push(base); + while (!queue.empty()) { + const db::INode& node = queue.top(); + if (node.getValueOfIsFile() == 0) { + auto children = api::fs::get_children(node); + queue.pop(); + for (const auto& child : children) { + if (child.getValueOfIsFile() == 0) queue.push(child); + else if (child.getSize()) size += child.getValueOfSize(); + } + } else { + size += node.getValueOfSize(); + queue.pop(); + } + } + return size; + } + + void fs::add_to_zip(struct zip_t* zip, const std::string& key, const db::INode& node, const std::string& path) { + if (node.getValueOfIsFile() == 0) { + std::string new_path = path + node.getValueOfName() + "/"; + zip_entry_opencasesensitive(zip, new_path.c_str()); + zip_entry_close(zip); + auto children = api::fs::get_children(node); + for (const auto& child : children) + add_to_zip(zip, key, child, new_path); + } else { + zip_entry_opencasesensitive(zip, (path + node.getValueOfName()).c_str()); + std::ifstream file("./files/" + std::to_string(node.getValueOfId()), std::ifstream::binary); + std::vector buffer(64*1024); + while (!file.eof()) { + file.read(buffer.data(), (std::streamsize)buffer.size()); + auto read = file.gcount(); + zip_entry_write(zip, buffer.data(), read); + std::get<1>(in_progress_zips[key]) += read; + } + zip_entry_close(zip); + } + } +} diff --git a/backend/src/controllers/fs.cpp b/backend/src/controllers/fs/fs_routes.cpp similarity index 53% rename from backend/src/controllers/fs.cpp rename to backend/src/controllers/fs/fs_routes.cpp index 24ae16a..6aea185 100644 --- a/backend/src/controllers/fs.cpp +++ b/backend/src/controllers/fs/fs_routes.cpp @@ -1,575 +1,345 @@ -#pragma clang diagnostic push -#pragma ide diagnostic ignored "performance-unnecessary-value-param" -#pragma ide diagnostic ignored "readability-convert-member-functions-to-static" - -#include -#include -#include - -#include -#include -#include -#include - -#include "controllers.h" -#include "dto/dto.h" - -char windows_invalid_chars[] = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F<>:\"/\\|"; - -// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#common_image_file_types -const std::unordered_map mime_type_map = { - { ".apng" , "image/apng" }, - { ".avif" , "image/avif" }, - { ".bmp" , "image/bmp" }, - { ".gif" , "image/gif" }, - { ".jpg" , "image/jpeg" }, - { ".jpeg" , "image/jpeg" }, - { ".jfif" , "image/jpeg" }, - { ".pjpeg", "image/jpeg" }, - { ".pjp" , "image/jpeg" }, - { ".png" , "image/png" }, - { ".svg" , "image/svg" }, - { ".webp" , "image/webp" }, - - { ".aac" , "audio/aac" }, - { ".flac" , "audio/flac" }, - { ".mp3" , "audio/mp3" }, - { ".m4a" , "audio/mp4" }, - { ".oga" , "audio/ogg" }, - { ".ogg" , "audio/ogg" }, - { ".wav" , "audio/wav" }, - - { ".3gp" , "video/3gpp" }, - { ".mpg" , "video/mpeg" }, - { ".mpeg" , "video/mpeg" }, - { ".mp4" , "video/mp4" }, - { ".m4v" , "video/mp4" }, - { ".m4p" , "video/mp4" }, - { ".ogv" , "video/ogg" }, - { ".mov" , "video/quicktime" }, - { ".webm" , "video/webm" }, - { ".mkv" , "video/x-matroska" }, - { ".mk3d" , "video/x-matroska" }, - { ".mks" , "video/x-matroska" }, -}; - -uint64_t next_temp_id = 0; -std::unordered_map zip_to_temp_map; -std::unordered_map> in_progress_zips; - -trantor::EventLoop* get_zip_loop() { - static bool init_done = false; - static trantor::EventLoopThread loop("ZipEventLoop"); - if (!init_done) { - init_done = true; - loop.run(); - loop.getLoop()->runEvery(30*60, []{ - for (const auto& entry : std::filesystem::directory_iterator("./temp")) { - if (!entry.is_regular_file()) continue; - const std::string file_name = "./temp/" + entry.path().filename().string(); - const auto& progress_pos = std::find_if(in_progress_zips.begin(), in_progress_zips.end(), - [&file_name](const std::pair>& entry) { - return std::get<0>(entry.second) == file_name; - } - ); - if (progress_pos != in_progress_zips.end()) return; - const auto& zip_map_pos = std::find_if(zip_to_temp_map.begin(), zip_to_temp_map.end(), - [&file_name](const std::pair& entry){ - return entry.second == file_name; - } - ); - if (zip_map_pos != zip_to_temp_map.end()) return; - std::filesystem::remove(entry.path()); - } - }); - } - return loop.getLoop(); -} - -trantor::EventLoop* get_delete_loop() { - static bool init_done = false; - static trantor::EventLoopThread loop("DeleteEventLoop"); - if (!init_done) { - init_done = true; - loop.run(); - } - return loop.getLoop(); -} - -void generate_path(db::INode node, std::string& str) { - db::MapperInode inode_mapper(drogon::app().getDbClient()); - std::stack path; - path.push(node); - while (node.getParentId() != nullptr) { - node = inode_mapper.findByPrimaryKey(node.getValueOfParentId()); - path.push(node); - } - while (!path.empty()) { - const db::INode& seg = path.top(); - str += seg.getValueOfName(); - if (seg.getValueOfIsFile() == 0) str += "/"; - path.pop(); - } -} - -Json::Value generate_path(db::INode node) { - Json::Value segments = Json::Value(Json::ValueType::arrayValue); - db::MapperInode inode_mapper(drogon::app().getDbClient()); - std::stack path; - path.push(node); - while (node.getParentId() != nullptr) { - node = inode_mapper.findByPrimaryKey(node.getValueOfParentId()); - path.push(node); - } - while (!path.empty()) { - const db::INode& seg = path.top(); - if (seg.getParentId() == nullptr) { - Json::Value json_seg; - json_seg["path"] = "/"; - json_seg["node"] = seg.getValueOfId(); - segments.append(json_seg); - } else { - Json::Value json_seg; - json_seg["path"] = seg.getValueOfName(); - json_seg["node"] = seg.getValueOfId(); - segments.append(json_seg); - if (seg.getValueOfIsFile() == 0) { - json_seg.removeMember("node"); - json_seg["path"] = "/"; - segments.append(json_seg); - } - } - path.pop(); - } - Json::Value resp; - resp["segments"] = segments; - return resp; -} - -uint64_t calc_total_size(const db::INode& base) { - uint64_t size = 0; - std::stack queue; - queue.push(base); - while (!queue.empty()) { - const db::INode& node = queue.top(); - if (node.getValueOfIsFile() == 0) { - auto children = api::fs::get_children(node); - queue.pop(); - for (const auto& child : children) { - if (child.getValueOfIsFile() == 0) queue.push(child); - else if (child.getSize()) size += child.getValueOfSize(); - } - } else { - size += node.getValueOfSize(); - queue.pop(); - } - } - return size; -} - -void add_to_zip(struct zip_t* zip, const std::string& key, const db::INode& node, const std::string& path) { - if (node.getValueOfIsFile() == 0) { - std::string new_path = path + node.getValueOfName() + "/"; - zip_entry_opencasesensitive(zip, new_path.c_str()); - zip_entry_close(zip); - auto children = api::fs::get_children(node); - for (const auto& child : children) - add_to_zip(zip, key, child, new_path); - } else { - zip_entry_opencasesensitive(zip, (path + node.getValueOfName()).c_str()); - std::ifstream file("./files/" + std::to_string(node.getValueOfId()), std::ifstream::binary); - std::vector buffer(64*1024); - while (!file.eof()) { - file.read(buffer.data(), (std::streamsize)buffer.size()); - auto read = file.gcount(); - zip_entry_write(zip, buffer.data(), read); - std::get<1>(in_progress_zips[key]) += read; - } - zip_entry_close(zip); - } -} - -template -std::string join_string(InputIt first, InputIt last, const std::string& separator = ",") { - std::ostringstream result; - if (first != last) { - result << *first; - while (++first != last) { - result << separator << *first; - } - } - return result.str(); -} - -namespace api { - std::optional fs::get_node(uint64_t node) { - db::MapperInode inode_mapper(drogon::app().getDbClient()); - try { - return inode_mapper.findByPrimaryKey(node); - } catch (const std::exception&) { - return std::nullopt; - } - } - - std::optional fs::get_node_and_validate(const db::User &user, uint64_t node) { - auto inode = get_node(node); - if (!inode.has_value()) return std::nullopt; - if (inode->getValueOfOwnerId() != user.getValueOfId()) return std::nullopt; - return inode; - } - - std::vector fs::get_children(const db::INode& parent) { - db::MapperInode inode_mapper(drogon::app().getDbClient()); - return inode_mapper.findBy(db::Criteria(db::INode::Cols::_parent_id, db::CompareOps::EQ, parent.getValueOfId())); - } - - std::variant> - fs::create_node(std::string name, const db::User& owner, bool file, const std::optional &parent, bool force) { - // Stolen from https://github.com/boostorg/filesystem/blob/develop/src/portability.cpp - if (!force) - if (name.empty() || name[0] == ' ' || name.find_first_of(windows_invalid_chars, 0, sizeof(windows_invalid_chars)) != std::string::npos || *(name.end() - 1) == ' ' || *(name.end() - 1) == '.' || name == "." || name == "..") - return {create_node_error::INVALID_NAME}; - - db::INode node; - node.setIsFile(file ? 1 : 0); - node.setName(name); - node.setOwnerId(owner.getValueOfId()); - node.setHasPreview(0); - if (parent.has_value()) { - auto parent_node = get_node_and_validate(owner, *parent); - if (!parent_node.has_value()) - return {create_node_error::INVALID_PARENT}; - if (parent_node->getValueOfIsFile() != 0) - return {create_node_error::FILE_PARENT}; - auto children = get_children(*parent_node); - for (const auto& child : children) - if (child.getValueOfName() == name) - return {std::make_tuple( - child.getValueOfIsFile() != 0, - child.getValueOfId() - )}; - node.setParentId(*parent); - } - db::MapperInode inode_mapper(drogon::app().getDbClient()); - inode_mapper.insert(node); - return {node}; - } - - void fs::delete_node(db::INode node, msd::channel& chan, bool allow_root) { - if (node.getValueOfParentId() == 0 && (!allow_root)) return; - - db::MapperInode inode_mapper(drogon::app().getDbClient()); - - const auto delete_file = [&chan, &inode_mapper](const db::INode& node) { - std::string entry = "Deleting "; - generate_path(node, entry); - entry >> chan; - std::filesystem::path p("./files"); - p /= std::to_string(node.getValueOfId()); - std::filesystem::remove(p); - if (node.getValueOfHasPreview() != 0) - std::filesystem::remove(p.string() + "_preview.png"); - inode_mapper.deleteOne(node); - std::string(" Done\n") >> chan; - }; - - std::stack queue, files, folders; - - if (node.getValueOfIsFile() == 0) queue.push(node); - else files.push(node); - - while (!queue.empty()) { - while (!files.empty()) { - delete_file(files.top()); - files.pop(); - } - std::string entry = "Deleting "; - generate_path(queue.top(), entry); - entry += "\n"; - entry >> chan; - auto children = get_children(queue.top()); - folders.push(queue.top()); - queue.pop(); - for (const auto& child : children) { - if (child.getValueOfIsFile() == 0) queue.push(child); - else files.push(child); - } - } - - while (!files.empty()) { - delete_file(files.top()); - files.pop(); - } - - while (!folders.empty()) { - inode_mapper.deleteOne(folders.top()); - folders.pop(); - } - } - - void fs::root(req_type req, cbk_type cbk) { - db::User user = dto::get_user(req); - cbk(dto::Responses::get_root_res(user.getValueOfRootId())); - } - - void fs::node(req_type req, cbk_type cbk, uint64_t node) { - db::User user = dto::get_user(req); - auto inode = get_node_and_validate(user, node); - if (!inode.has_value()) - return cbk(dto::Responses::get_badreq_res("Unknown node")); - auto dto_node = dto::Responses::GetNodeEntry(*inode); - std::vector children; - if (!dto_node.is_file) for (const db::INode& child : get_children(*inode)) children.emplace_back(child); - cbk(dto::Responses::get_node_res(dto_node, children)); - } - - void fs::path(req_type req, cbk_type cbk, uint64_t node) { - db::User user = dto::get_user(req); - auto inode = get_node_and_validate(user, node); - if (!inode.has_value()) - cbk(dto::Responses::get_badreq_res("Unknown node")); - else { - auto path = generate_path(*inode); - cbk(dto::Responses::get_success_res(path)); - } - } - - template - void fs::create_node_req(req_type req, cbk_type cbk) { - db::User user = dto::get_user(req); - Json::Value& json = *req->jsonObject(); - try { - uint64_t parent = dto::json_get(json, "parent").value(); - std::string name = dto::json_get(json, "name").value(); - - auto new_node = create_node(name, user, file, std::make_optional(parent)); - if (std::holds_alternative(new_node)) - cbk(dto::Responses::get_new_node_res(std::get(new_node).getValueOfId())); - else if (std::holds_alternative(new_node)) - switch (std::get(new_node)) { - case create_node_error::INVALID_NAME: return cbk(dto::Responses::get_badreq_res("Invalid name")); - case create_node_error::INVALID_PARENT: return cbk(dto::Responses::get_badreq_res("Invalid parent")); - case create_node_error::FILE_PARENT: return cbk(dto::Responses::get_badreq_res("Parent is file")); - } - else { - auto tuple = std::get>(new_node); - cbk(dto::Responses::get_node_exists_res(std::get<1>(tuple), std::get<0>(tuple))); - } - } catch (const std::exception&) { - cbk(dto::Responses::get_badreq_res("Validation error")); - } - } - - void fs::delete_node_req(req_type req, cbk_type cbk, uint64_t node) { - db::User user = dto::get_user(req); - auto inode = get_node_and_validate(user, node); - if (!inode.has_value()) - cbk(dto::Responses::get_badreq_res("Unknown node")); - else if (inode->getValueOfParentId() == 0) - cbk(dto::Responses::get_badreq_res("Can't delete root")); - else { - auto chan = std::make_shared>(); - std::string("Waiting in queue...\n") >> (*chan); - get_delete_loop()->queueInLoop([chan, inode=*inode]{ - delete_node(inode, *chan); - chan->close(); - }); - cbk(drogon::HttpResponse::newStreamResponse([chan](char* buf, std::size_t size) -> std::size_t{ - if (buf == nullptr) return 0; - if (chan->closed() && chan->empty()) return 0; - std::string buffer; - buffer << *chan; - if (buffer.empty()) return 0; - std::size_t read = std::min(size, buffer.size()); - std::memcpy(buf, buffer.data(), read); // NOLINT(bugprone-not-null-terminated-result) - return read; - })); - } - } - - void fs::upload(req_type req, cbk_type cbk, uint64_t node) { - constexpr int image_height = 256; - db::User user = dto::get_user(req); - - auto inode = get_node_and_validate(user, node); - if (!inode.has_value()) - return cbk(dto::Responses::get_badreq_res("Unknown node")); - if (inode->getValueOfIsFile() == 0) - return cbk(dto::Responses::get_badreq_res("Can't upload to a directory")); - - drogon::MultiPartParser mpp; - if (mpp.parse(req) != 0) - return cbk(dto::Responses::get_badreq_res("Failed to parse files")); - if (mpp.getFiles().size() != 1) - return cbk(dto::Responses::get_badreq_res("Exactly 1 file needed")); - - const drogon::HttpFile& file = mpp.getFiles().at(0); - - std::filesystem::path p("./files"); - p /= std::to_string(inode->getValueOfId()); - - file.saveAs(p.string()); - try { - if (file.fileLength() > 100 * 1024 * 1024) throw std::exception(); - std::filesystem::path filename(inode->getValueOfName()); - const std::string& mime = mime_type_map.at(filename.extension().string()); - if (!mime.starts_with("image")) throw std::exception(); - - cv::_InputArray image_arr(file.fileData(), (int) file.fileLength()); - cv::Mat image = cv::imdecode(image_arr, cv::IMREAD_COLOR); - if (!image.empty()) { - float h_ration = ((float) image_height) / ((float) image.rows); - cv::Mat preview; - cv::resize(image, preview, cv::Size((int) (((float) image.cols) * h_ration), image_height), 0, 0, cv::INTER_AREA); - cv::imwrite(p.string() + "_preview.png", preview); - inode->setHasPreview(1); - } - } catch (const std::exception&) {} - inode->setSize(file.fileLength()); - - db::MapperInode inode_mapper(drogon::app().getDbClient()); - inode_mapper.update(*inode); - - cbk(dto::Responses::get_success_res()); - } - - void fs::create_zip(req_type req, cbk_type cbk) { - db::User user = dto::get_user(req); - Json::Value& json = *req->jsonObject(); - try { - if (!json.isMember("nodes")) throw std::exception(); - Json::Value node_arr = json["nodes"]; - if (!node_arr.isArray()) throw std::exception(); - std::vector node_ids; - for (const auto& node : node_arr) - node_ids.push_back(node.asUInt64()); - - std::vector nodes; - std::transform(node_ids.begin(), node_ids.end(), std::back_inserter(nodes), [&user](uint64_t node) { - return api::fs::get_node_and_validate(user, node).value(); - }); - - std::string key = join_string(node_ids.begin(), node_ids.end()); - - if (zip_to_temp_map.contains(key)) return cbk(dto::Responses::get_create_zip_done_res()); - if (in_progress_zips.contains(key)) { - auto progress = in_progress_zips.at(key); - return cbk(dto::Responses::get_create_zip_done_res(std::get<1>(progress), std::get<2>(progress))); - } - uint64_t size = 0; - for (const auto& node : nodes) size += calc_total_size(node); - std::string file_name = "./temp/fs_" + std::to_string(next_temp_id++) + ".zip"; - in_progress_zips.emplace(key, std::make_tuple(file_name, 0, size)); - get_zip_loop()->queueInLoop([key = std::move(key), nodes = std::move(nodes), file_name = std::move(file_name)]{ - { - struct zip_t* zip = zip_open(file_name.c_str(), ZIP_DEFAULT_COMPRESSION_LEVEL, 'w'); - for (const db::INode& node : nodes) - add_to_zip(zip, key, node, ""); - zip_close(zip); - } - zip_to_temp_map.emplace(key, file_name); - in_progress_zips.erase(key); - }); - return cbk(dto::Responses::get_create_zip_done_res(0, size)); - } catch (const std::exception&) { - cbk(dto::Responses::get_badreq_res("Validation error")); - } - } - - void fs::download(req_type req, cbk_type cbk) { - db::User user = dto::get_user(req); - - auto node_id = req->getOptionalParameter("id"); - if (!node_id.has_value()) { - cbk(dto::Responses::get_badreq_res("Invalid node")); - return; - } - auto inode = get_node_and_validate(user, *node_id); - if (!inode.has_value()) { - cbk(dto::Responses::get_badreq_res("Invalid node")); - return; - } - - if (inode->getValueOfIsFile() != 0) { - std::filesystem::path p("./files"); - p /= std::to_string(inode->getValueOfId()); - - cbk(drogon::HttpResponse::newFileResponse( - p.string(), - inode->getValueOfName() - )); - } else { - try { - std::string key = std::to_string(inode->getValueOfId()); - std::string file = zip_to_temp_map.at(key); - zip_to_temp_map.erase(key); - cbk(drogon::HttpResponse::newFileResponse( - file, - inode->getValueOfName() + ".zip" - )); - } catch (const std::exception&) { - cbk(dto::Responses::get_badreq_res("Invalid node")); - } - } - } - - void fs::download_multi(req_type req, cbk_type cbk) { - db::User user = dto::get_user(req); - - auto node_ids_str = req->getOptionalParameter("id"); - if (!node_ids_str.has_value()) - return cbk(dto::Responses::get_badreq_res("No nodes")); - - std::stringstream node_ids_ss(*node_ids_str); - std::string temp; - try { - while (std::getline(node_ids_ss, temp, ',')) - if (!get_node_and_validate(user, std::stoull(temp)).has_value()) throw std::exception(); - - std::string file = zip_to_temp_map.at(*node_ids_str); - zip_to_temp_map.erase(*node_ids_str); - cbk(drogon::HttpResponse::newFileResponse( - file, - "files.zip" - )); - } catch (const std::exception&) { - cbk(dto::Responses::get_badreq_res("Invalid nodes")); - } - } - - void fs::download_preview(req_type req, cbk_type cbk, uint64_t node) { - db::User user = dto::get_user(req); - - auto inode = get_node_and_validate(user, node); - if (!inode.has_value()) - return cbk(dto::Responses::get_badreq_res("Unknown node")); - if (inode->getValueOfHasPreview() == 0) - return cbk(dto::Responses::get_badreq_res("No preview")); - - std::filesystem::path p("./files"); - p /= std::to_string(inode->getValueOfId()) + "_preview.png"; - std::ifstream file(p, std::ios::in | std::ios::binary); - std::vector image((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - - cbk(dto::Responses::get_download_base64_res("data:image/png;base64," + Botan::base64_encode(image))); - } - - void fs::get_type(req_type req, cbk_type cbk, uint64_t node){ - db::User user = dto::get_user(req); - - auto inode = get_node_and_validate(user, node); - if (!inode.has_value()) - return cbk(dto::Responses::get_badreq_res("Unknown node")); - - - std::filesystem::path p("./files"), name(inode->getValueOfName()); - p /= std::to_string(inode->getValueOfId()); - - try { - cbk(dto::Responses::get_type_res(mime_type_map.at(name.extension().string()))); - } catch (const std::exception&) { - cbk(dto::Responses::get_badreq_res("Invalid file type")); - } - } -} -#pragma clang diagnostic pop \ No newline at end of file +#include +#include + +#include +#include + +#include "controllers/controllers.h" +#include "dto/dto.h" + +// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#common_image_file_types +const std::unordered_map mime_type_map = { + { ".apng" , "image/apng" }, + { ".avif" , "image/avif" }, + { ".bmp" , "image/bmp" }, + { ".gif" , "image/gif" }, + { ".jpg" , "image/jpeg" }, + { ".jpeg" , "image/jpeg" }, + { ".jfif" , "image/jpeg" }, + { ".pjpeg", "image/jpeg" }, + { ".pjp" , "image/jpeg" }, + { ".png" , "image/png" }, + { ".svg" , "image/svg" }, + { ".webp" , "image/webp" }, + + { ".aac" , "audio/aac" }, + { ".flac" , "audio/flac" }, + { ".mp3" , "audio/mp3" }, + { ".m4a" , "audio/mp4" }, + { ".oga" , "audio/ogg" }, + { ".ogg" , "audio/ogg" }, + { ".wav" , "audio/wav" }, + + { ".3gp" , "video/3gpp" }, + { ".mpg" , "video/mpeg" }, + { ".mpeg" , "video/mpeg" }, + { ".mp4" , "video/mp4" }, + { ".m4v" , "video/mp4" }, + { ".m4p" , "video/mp4" }, + { ".ogv" , "video/ogg" }, + { ".mov" , "video/quicktime" }, + { ".webm" , "video/webm" }, + { ".mkv" , "video/x-matroska" }, + { ".mk3d" , "video/x-matroska" }, + { ".mks" , "video/x-matroska" }, +}; + +template +std::string join_string(InputIt first, InputIt last, const std::string& separator = ",") { + std::ostringstream result; + if (first != last) { + result << *first; + while (++first != last) { + result << separator << *first; + } + } + return result.str(); +} + +namespace api { + void fs::root(req_type req, cbk_type cbk) { + db::User user = dto::get_user(req); + cbk(dto::Responses::get_root_res(user.getValueOfRootId())); + } + + void fs::node(req_type req, cbk_type cbk, uint64_t node) { + db::User user = dto::get_user(req); + std::shared_lock lock(*get_user_mutex(user.getValueOfId())); + auto inode = get_node_and_validate(user, node); + if (!inode.has_value()) + return cbk(dto::Responses::get_badreq_res("Unknown node")); + auto dto_node = dto::Responses::GetNodeEntry(*inode); + std::vector children; + if (!dto_node.is_file) for (const db::INode& child : get_children(*inode)) children.emplace_back(child); + cbk(dto::Responses::get_node_res(dto_node, children)); + } + + void fs::path(req_type req, cbk_type cbk, uint64_t node) { + db::User user = dto::get_user(req); + std::shared_lock lock(*get_user_mutex(user.getValueOfId())); + auto inode = get_node_and_validate(user, node); + if (!inode.has_value()) + cbk(dto::Responses::get_badreq_res("Unknown node")); + else { + auto path = generate_path(*inode); + cbk(dto::Responses::get_success_res(path)); + } + } + + template + void fs::create_node_req(req_type req, cbk_type cbk) { + db::User user = dto::get_user(req); + Json::Value& json = *req->jsonObject(); + try { + uint64_t parent = dto::json_get(json, "parent").value(); + std::string name = dto::json_get(json, "name").value(); + + std::shared_lock lock(*get_user_mutex(user.getValueOfId())); + + auto new_node = create_node(name, user, file, std::make_optional(parent)); + if (std::holds_alternative(new_node)) + cbk(dto::Responses::get_new_node_res(std::get(new_node).getValueOfId())); + else if (std::holds_alternative(new_node)) + switch (std::get(new_node)) { + case create_node_error::INVALID_NAME: return cbk(dto::Responses::get_badreq_res("Invalid name")); + case create_node_error::INVALID_PARENT: return cbk(dto::Responses::get_badreq_res("Invalid parent")); + case create_node_error::FILE_PARENT: return cbk(dto::Responses::get_badreq_res("Parent is file")); + } + else { + auto tuple = std::get>(new_node); + cbk(dto::Responses::get_node_exists_res(std::get<1>(tuple), std::get<0>(tuple))); + } + } catch (const std::exception&) { + cbk(dto::Responses::get_badreq_res("Validation error")); + } + } + + void fs::delete_node_req(req_type req, cbk_type cbk, uint64_t node) { + db::User user = dto::get_user(req); + std::unique_lock lock(*get_user_mutex(user.getValueOfId())); + auto inode = get_node_and_validate(user, node); + if (!inode.has_value()) + cbk(dto::Responses::get_badreq_res("Unknown node")); + else if (inode->getValueOfParentId() == 0) + cbk(dto::Responses::get_badreq_res("Can't delete root")); + else { + auto chan = std::make_shared>(); + std::string("Waiting in queue...\n") >> (*chan); + get_delete_loop()->queueInLoop([chan, inode=*inode, user=user.getValueOfId()]{ + std::unique_lock lock(*get_user_mutex(user)); + delete_node(inode, *chan); + chan->close(); + }); + cbk(drogon::HttpResponse::newStreamResponse([chan](char* buf, std::size_t size) -> std::size_t{ + if (buf == nullptr) return 0; + if (chan->closed() && chan->empty()) return 0; + std::string buffer; + buffer << *chan; + if (buffer.empty()) return 0; + std::size_t read = std::min(size, buffer.size()); + std::memcpy(buf, buffer.data(), read); // NOLINT(bugprone-not-null-terminated-result) + return read; + })); + } + } + + void fs::upload(req_type req, cbk_type cbk, uint64_t node) { + constexpr int image_height = 256; + db::User user = dto::get_user(req); + + std::shared_lock lock(*get_user_mutex(user.getValueOfId())); + + auto inode = get_node_and_validate(user, node); + if (!inode.has_value()) + return cbk(dto::Responses::get_badreq_res("Unknown node")); + if (inode->getValueOfIsFile() == 0) + return cbk(dto::Responses::get_badreq_res("Can't upload to a directory")); + + drogon::MultiPartParser mpp; + if (mpp.parse(req) != 0) + return cbk(dto::Responses::get_badreq_res("Failed to parse files")); + if (mpp.getFiles().size() != 1) + return cbk(dto::Responses::get_badreq_res("Exactly 1 file needed")); + + const drogon::HttpFile& file = mpp.getFiles().at(0); + + std::filesystem::path p("./files"); + p /= std::to_string(inode->getValueOfId()); + + file.saveAs(p.string()); + try { + if (file.fileLength() > 100 * 1024 * 1024) throw std::exception(); + std::filesystem::path filename(inode->getValueOfName()); + const std::string& mime = mime_type_map.at(filename.extension().string()); + if (!mime.starts_with("image")) throw std::exception(); + + cv::_InputArray image_arr(file.fileData(), (int) file.fileLength()); + cv::Mat image = cv::imdecode(image_arr, cv::IMREAD_COLOR); + if (!image.empty()) { + float h_ration = ((float) image_height) / ((float) image.rows); + cv::Mat preview; + cv::resize(image, preview, cv::Size((int) (((float) image.cols) * h_ration), image_height), 0, 0, cv::INTER_AREA); + cv::imwrite(p.string() + "_preview.png", preview); + inode->setHasPreview(1); + } + } catch (const std::exception&) {} + inode->setSize(file.fileLength()); + + db::MapperInode inode_mapper(drogon::app().getDbClient()); + inode_mapper.update(*inode); + + cbk(dto::Responses::get_success_res()); + } + + void fs::create_zip(req_type req, cbk_type cbk) { + db::User user = dto::get_user(req); + + std::shared_lock lock(*get_user_mutex(user.getValueOfId())); + + Json::Value& json = *req->jsonObject(); + try { + if (!json.isMember("nodes")) throw std::exception(); + Json::Value node_arr = json["nodes"]; + if (!node_arr.isArray()) throw std::exception(); + std::vector node_ids; + for (const auto& node : node_arr) + node_ids.push_back(node.asUInt64()); + + std::vector nodes; + std::transform(node_ids.begin(), node_ids.end(), std::back_inserter(nodes), [&user](uint64_t node) { + return api::fs::get_node_and_validate(user, node).value(); + }); + + std::string key = join_string(node_ids.begin(), node_ids.end()); + + if (zip_to_temp_map.contains(key)) return cbk(dto::Responses::get_create_zip_done_res()); + if (in_progress_zips.contains(key)) { + auto progress = in_progress_zips.at(key); + return cbk(dto::Responses::get_create_zip_done_res(std::get<1>(progress), std::get<2>(progress))); + } + std::string file_name = "./temp/fs_" + std::to_string(next_temp_id++) + ".zip"; + in_progress_zips.emplace(key, std::make_tuple(file_name, 0, 1)); + get_zip_loop()->queueInLoop([key = std::move(key), nodes = std::move(nodes), file_name = std::move(file_name), user=user.getValueOfId()]{ + { + std::shared_lock lock(*get_user_mutex(user)); + uint64_t size = 0; + for (const auto& node : nodes) size += calc_total_size(node); + std::get<2>(in_progress_zips.at(key)) = size; + struct zip_t* zip = zip_open(file_name.c_str(), ZIP_DEFAULT_COMPRESSION_LEVEL, 'w'); + for (const db::INode& node : nodes) + add_to_zip(zip, key, node, ""); + zip_close(zip); + } + zip_to_temp_map.emplace(key, file_name); + in_progress_zips.erase(key); + }); + return cbk(dto::Responses::get_create_zip_done_res(0, 1)); + } catch (const std::exception&) { + cbk(dto::Responses::get_badreq_res("Validation error")); + } + } + + void fs::download(req_type req, cbk_type cbk) { + db::User user = dto::get_user(req); + + std::shared_lock lock(*get_user_mutex(user.getValueOfId())); + + auto node_id = req->getOptionalParameter("id"); + if (!node_id.has_value()) { + cbk(dto::Responses::get_badreq_res("Invalid node")); + return; + } + auto inode = get_node_and_validate(user, *node_id); + if (!inode.has_value()) { + cbk(dto::Responses::get_badreq_res("Invalid node")); + return; + } + + if (inode->getValueOfIsFile() != 0) { + std::filesystem::path p("./files"); + p /= std::to_string(inode->getValueOfId()); + + cbk(drogon::HttpResponse::newFileResponse( + p.string(), + inode->getValueOfName() + )); + } else { + try { + std::string key = std::to_string(inode->getValueOfId()); + std::string file = zip_to_temp_map.at(key); + zip_to_temp_map.erase(key); + cbk(drogon::HttpResponse::newFileResponse( + file, + inode->getValueOfName() + ".zip" + )); + } catch (const std::exception&) { + cbk(dto::Responses::get_badreq_res("Invalid node")); + } + } + } + + void fs::download_multi(req_type req, cbk_type cbk) { + db::User user = dto::get_user(req); + + std::shared_lock lock(*get_user_mutex(user.getValueOfId())); + + auto node_ids_str = req->getOptionalParameter("id"); + if (!node_ids_str.has_value()) + return cbk(dto::Responses::get_badreq_res("No nodes")); + + std::stringstream node_ids_ss(*node_ids_str); + std::string temp; + try { + while (std::getline(node_ids_ss, temp, ',')) + if (!get_node_and_validate(user, std::stoull(temp)).has_value()) throw std::exception(); + + std::string file = zip_to_temp_map.at(*node_ids_str); + zip_to_temp_map.erase(*node_ids_str); + cbk(drogon::HttpResponse::newFileResponse( + file, + "files.zip" + )); + } catch (const std::exception&) { + cbk(dto::Responses::get_badreq_res("Invalid nodes")); + } + } + + void fs::download_preview(req_type req, cbk_type cbk, uint64_t node) { + db::User user = dto::get_user(req); + + std::shared_lock lock(*get_user_mutex(user.getValueOfId())); + + auto inode = get_node_and_validate(user, node); + if (!inode.has_value()) + return cbk(dto::Responses::get_badreq_res("Unknown node")); + if (inode->getValueOfHasPreview() == 0) + return cbk(dto::Responses::get_badreq_res("No preview")); + + std::filesystem::path p("./files"); + p /= std::to_string(inode->getValueOfId()) + "_preview.png"; + std::ifstream file(p, std::ios::in | std::ios::binary); + std::vector image((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + cbk(dto::Responses::get_download_base64_res("data:image/png;base64," + Botan::base64_encode(image))); + } + + void fs::get_type(req_type req, cbk_type cbk, uint64_t node){ + db::User user = dto::get_user(req); + + std::shared_lock lock(*get_user_mutex(user.getValueOfId())); + + auto inode = get_node_and_validate(user, node); + if (!inode.has_value()) + return cbk(dto::Responses::get_badreq_res("Unknown node")); + + + std::filesystem::path p("./files"), name(inode->getValueOfName()); + p /= std::to_string(inode->getValueOfId()); + + try { + cbk(dto::Responses::get_type_res(mime_type_map.at(name.extension().string()))); + } catch (const std::exception&) { + cbk(dto::Responses::get_badreq_res("Invalid file type")); + } + } +} \ No newline at end of file diff --git a/backend/src/controllers/user.cpp b/backend/src/controllers/user.cpp index d650726..b550cae 100644 --- a/backend/src/controllers/user.cpp +++ b/backend/src/controllers/user.cpp @@ -21,6 +21,9 @@ namespace api { msd::channel chan; db::User user = dto::get_user(req); auth::revoke_all(user); + + std::unique_lock lock(*fs::get_user_mutex(user.getValueOfId())); + fs::delete_node((fs::get_node(user.getValueOfRootId())).value(), chan, true); user_mapper.deleteOne(user);