diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e69c5ac..3142c8e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,7 +26,6 @@ build_backend: artifacts: paths: - server - expire_in: 1h test_and_build_frontend: image: node:latest @@ -44,7 +43,6 @@ test_and_build_frontend: artifacts: paths: - frontend/dist/ - expire_in: 1h package_server: stage: package diff --git a/backend/.idea/cmake.xml b/backend/.idea/cmake.xml new file mode 100644 index 0000000..590a81f --- /dev/null +++ b/backend/.idea/cmake.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/backend/.idea/dataSources.xml b/backend/.idea/dataSources.xml index 61eb0a1..78eab0c 100644 --- a/backend/.idea/dataSources.xml +++ b/backend/.idea/dataSources.xml @@ -1,18 +1,11 @@ - - sqlite.xerial - true - org.sqlite.JDBC - jdbc:sqlite:$PROJECT_DIR$/old_backend/sqlite.db - $ProjectFileDir$ - sqlite.xerial true org.sqlite.JDBC - jdbc:sqlite:$PROJECT_DIR$/run/sqlite.db + jdbc:sqlite:$PROJECT_DIR$/../run/sqlite.db $ProjectFileDir$ diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index 643a33b..2ed6e20 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -1,4 +1,10 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.21) + +if (WIN32 AND (NOT VCPKG_TARGET_TRIPLET)) + set(VCPKG_TARGET_TRIPLET x64-windows-static) +endif (WIN32 AND (NOT VCPKG_TARGET_TRIPLET)) +#set(VCPKG_LIBRARY_LINKAGE static) + project(backend) set(CMAKE_CXX_STANDARD 20) @@ -13,13 +19,6 @@ add_executable(backend src/db/db.h src/db/db.cpp - src/db/model/Inode.cc - src/db/model/Inode.h - src/db/model/Tokens.cc - src/db/model/Tokens.h - src/db/model/User.cc - src/db/model/User.h - src/controllers/controllers.h src/controllers/admin.cpp src/controllers/fs.cpp @@ -32,14 +31,23 @@ add_executable(backend src/filters/filters.h src/filters/filters.cpp + + model/Inode.cc + model/Inode.h + model/Tokens.cc + model/Tokens.h + model/User.cc + model/User.h + + SMTPMail-drogon-master/SMTPMail.cc ) find_package(Drogon CONFIG REQUIRED) -find_package(mailio CONFIG REQUIRED) -find_package(lodepng CONFIG REQUIRED) find_package(OpenSSL REQUIRED) +find_package(OpenCV CONFIG REQUIRED) +find_package(kubazip CONFIG REQUIRED) find_path(JWT_CPP_INCLUDE_DIRS "jwt-cpp/base.h") find_path(BOTAN_INCLUDE_DIRS "botan/botan.h") find_path(QR_INCLUDE_DIRS "qrcodegen.hpp") @@ -48,6 +56,10 @@ find_library(QR_LIBRARY nayuki-qr-code-generator) target_include_directories(backend PRIVATE src + model + shl + SMTPMail-drogon-master + ${OpenCV_INCLUDE_DIRS} ${JWT_CPP_INCLUDE_DIRS} ${BOTAN_INCLUDE_DIRS} ${QR_INCLUDE_DIRS} @@ -55,14 +67,17 @@ target_include_directories(backend PRIVATE target_link_libraries(backend Drogon::Drogon - mailio - lodepng OpenSSL::SSL + kubazip::kubazip + ${OpenCV_LIBS} ${BOTAN_LIBRARY} ${QR_LIBRARY} ) -install(TARGETS backend) +set_property(TARGET backend PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + +install(TARGETS backend RUNTIME_DEPENDENCY_SET backend_deps DESTINATION .) +install(RUNTIME_DEPENDENCY_SET backend_deps) if(NOT MSVC) target_compile_options(backend PRIVATE @@ -74,5 +89,6 @@ else() endif(NOT MSVC) if(WIN32) + target_link_libraries(backend iphlpapi) target_compile_definitions(backend PRIVATE NOMINMAX _WIN32_WINNT=0x0A00) endif() diff --git a/backend/SMTPMail-drogon-master/LICENSE b/backend/SMTPMail-drogon-master/LICENSE new file mode 100644 index 0000000..bd8c790 --- /dev/null +++ b/backend/SMTPMail-drogon-master/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 ihmc3jn09hk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/SMTPMail-drogon-master/README.md b/backend/SMTPMail-drogon-master/README.md new file mode 100644 index 0000000..5463867 --- /dev/null +++ b/backend/SMTPMail-drogon-master/README.md @@ -0,0 +1,79 @@ +# SMTPMail-drogon +Simple Mail for the Drogon framework. + +It is made as a plugin for the [drogon](https://github.com/an-tao/drogon) framework. +It can be included into the drogon build with little +modification of the class declaration. +## Updates +- **[ 13-06-2022 ] Fixed vulnerability issues reported by [Sam](https://snoopysecurity.github.io/about).** +- [ 13-09-2021 ] Added [HTML content support](https://github.com/ihmc3jn09hk/SMTPMail-drogon/pull/1). +- [ 23-12-2020 ] Added DNS support. + +## Acknowledgement +* The implementation takes SMTPClient for Qt from [kelvins](https://github.com/kelvins/SMTPClient) as reference. +* There requires a delay SSL encryption from the Tcp-socket (named TcpClient in trantor/drogon) and the major +author of drogon [reponsed](https://github.com/an-tao/drogon/issues/346) quickly. + +## Usage +Download to the plugin directory of the target drogon app, E.g. ~/drogon-app/plugins +```bash +$ git clone https://github.com/ihmc3jn09hk/SMTPMail-drogon.git +$ cp SMTPMail-drogon/SMTPMail.* ~/drogon-app/plugins +``` + +* _Be aware of add the plugin into the config.json. Set the "name" field to "SMTPMail"_ + +Add the reference header and get the plugin from the app(), E.g. + +```c++ +... +#include "../plugins/SMTPMail.h" +... + +//Inside some function, E.g. A controller function. +... +//Send an email +auto *smtpmailPtr = app().getPlugin(); +auto id = smtpmailPtr->sendEmail( + "127.0.0.1", //The server IP/DNS + 587, //The port + "mailer@something.com", //Who send the email + "receiver@otherthing.com", //Send to whom + "Testing SMTPMail Function", //Email Subject/Title + "Hello from drogon plugin", //Content + "mailer@something.com", //Login user + "123456", //User password + false //Is HTML content + ); +... +//Or get noted when email is sent +... +void callback(const std::string &msg) +{ + LOG_INFO << msg; /*Output e.g. "EMail sent. ID : 96ESERVDDFH17588ECF0C7B00326E3"*/ + /*Do whatever you like*/ +} +... +auto *smtpmailPtr = app().getPlugin(); +auto id = smtpmailPtr->sendEmail( + "127.0.0.1", //The server IP/DNS + 587, //The port + "mailer@something.com", //Who send the email + "receiver@otherthing.com", //Send to whom + "Testing SMTPMail Function", //Email Subject/Title + "Hello from drogon plugin", //Content + "mailer@something.com", //Login user + "123456", //User password + false, //Is HTML content + callback //Callback + ); +``` + +```bash +$ cd ~/drogon-app/build +$ make +``` + +## Licence +* Feel free to use, thanks to open-source. +* For the sake of concern on commercial usage, a simple licence is included in each of the files. diff --git a/backend/SMTPMail-drogon-master/SMTPMail.cc b/backend/SMTPMail-drogon-master/SMTPMail.cc new file mode 100644 index 0000000..8b105c2 --- /dev/null +++ b/backend/SMTPMail-drogon-master/SMTPMail.cc @@ -0,0 +1,400 @@ +/** +* +* SMTPMail.cc * +* +* This plugin is for SMTP mail delivery for the Drogon web-framework. +Implementation +* reference from the project "SMTPClient" with Qt5 by kelvins. Please check out +* https://github.com/kelvins/SMTPClient. + +Feel free to use the code. For the sake of any concern, the following licence is +attached. + + Copyright 2020 ihmc3jn09hk + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + */ + +#include "SMTPMail.h" +#include +#include +#include +#include + +using namespace drogon; +using namespace trantor; + +struct EMail { + enum states { + Init, + HandShake, + Tls, + Auth, + User, + Pass, + Mail, + Rcpt, + Data, + Body, + Quit, + Close + }; + std::string m_from; + std::string m_to; + std::string m_subject; + std::string m_content; + std::string m_user; + std::string m_passwd; + states m_status; + std::string m_uuid; + bool m_isHTML{false}; + std::shared_ptr m_socket; + + EMail(std::string from, std::string to, std::string subject, + std::string content, std::string user, std::string passwd, bool isHTML, + std::shared_ptr socket) + : m_from(std::move(from)), m_to(std::move(to)), + m_subject(std::move(subject)), m_content(std::move(content)), + m_user(std::move(user)), m_passwd(std::move(passwd)), + m_socket(std::move(socket)), m_isHTML(isHTML), + m_uuid(drogon::utils::getUuid()) { + m_status = Init; + } + + ~EMail() = default; + + static std::unordered_map> + m_emails; // Container for processing emails +}; + +std::unordered_map> EMail::m_emails; + +void SMTPMail::initAndStart(const Json::Value &config) { + /// Initialize and start the plugin + LOG_INFO << "SMTPMail initialized and started"; +} + +void SMTPMail::shutdown() { + /// Shutdown the plugin + LOG_INFO << "STMPMail shutdown"; +} + +void messagesHandle(const trantor::TcpConnectionPtr &connPtr, + trantor::MsgBuffer *msg, + const std::shared_ptr &email, + const std::function &cb) { + std::string receivedMsg; + while (msg->readableBytes() > 0) { + std::string buf(msg->peek(), msg->readableBytes()); + receivedMsg.append(buf); + // LOG_INFO << buf; + msg->retrieveAll(); + } + LOG_TRACE << "receive: " << receivedMsg; + std::string responseCode(receivedMsg.begin(), receivedMsg.begin() + 3); + // std::string responseMsg(receivedMsg.begin() + 4, receivedMsg.end()); + + if (email->m_status == EMail::Init && responseCode == "220") { + std::string outMsg; + trantor::MsgBuffer out; + + outMsg.append("EHLO smtpclient.qw"); + outMsg.append("\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->send(std::move(out)); + + email->m_status = EMail::HandShake; + } else if (email->m_status == EMail::HandShake && responseCode == "220") { + std::string outMsg; + trantor::MsgBuffer out; + + outMsg.append("EHLO smtpclient.qw"); + outMsg.append("\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->startClientEncryption( + [connPtr, out]() { + // LOG_TRACE << "SSL established"; + connPtr->send(out); + }, + false, false); + + email->m_status = EMail::Auth; + } else if (email->m_status == EMail::HandShake && responseCode == "250") { + std::string outMsg; + trantor::MsgBuffer out; + + outMsg.append("STARTTLS"); + outMsg.append("\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->send(std::move(out)); + + email->m_status = EMail::HandShake; + } else if (email->m_status == EMail::Auth && responseCode == "250") { + trantor::MsgBuffer out; + std::string outMsg; + + outMsg.append("AUTH LOGIN"); + outMsg.append("\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->send(std::move(out)); + + email->m_status = EMail::User; + } else if (email->m_status == EMail::User && responseCode == "334") { + trantor::MsgBuffer out; + std::string outMsg; + + std::string secret(email->m_user); + + // outMsg.append(base64_encode(reinterpret_cast(secret.c_str()), secret.length())); + outMsg.append(drogon::utils::base64Encode( + reinterpret_cast(secret.c_str()), + secret.length())); + + outMsg.append("\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->send(std::move(out)); + + email->m_status = EMail::Pass; + } else if (email->m_status == EMail::Pass && responseCode == "334") { + trantor::MsgBuffer out; + std::string outMsg; + + std::string secret(email->m_passwd); + + outMsg.append(drogon::utils::base64Encode( + reinterpret_cast(secret.c_str()), + secret.length())); + outMsg.append("\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->send(std::move(out)); + + email->m_status = EMail::Mail; + } else if (email->m_status == EMail::Mail && responseCode == "235") { + trantor::MsgBuffer out; + std::string outMsg; + + outMsg.append("MAIL FROM:<"); + outMsg.append(email->m_from); + outMsg.append(">\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->send(std::move(out)); + + email->m_status = EMail::Rcpt; + } else if (email->m_status == EMail::Rcpt && responseCode == "250") { + trantor::MsgBuffer out; + std::string outMsg; + + outMsg.append("RCPT TO:<"); + outMsg.append(email->m_to); + outMsg.append(">\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->send(std::move(out)); + + email->m_status = EMail::Data; + } else if (email->m_status == EMail::Data && responseCode == "250") { + trantor::MsgBuffer out; + std::string outMsg; + + outMsg.append("DATA"); + outMsg.append("\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->send(std::move(out)); + + email->m_status = EMail::Body; + } else if (email->m_status == EMail::Body && responseCode == "354") { + trantor::MsgBuffer out; + std::string outMsg; + std::time_t t = std::time(nullptr); + char buf[100]; + std::strftime(buf, 100, "%a, %d %b %Y %T %z", std::localtime(&t)); + + outMsg.append("To: " + email->m_to + "\r\n"); + outMsg.append("From: " + email->m_from + "\r\n"); + outMsg.append("Date: " + std::string(buf) + "\r\n"); + if (email->m_isHTML) { + outMsg.append("Content-Type: text/html;\r\n"); + } + outMsg.append("Subject: " + email->m_subject + "\r\n\r\n"); + + outMsg.append(email->m_content); + outMsg.append("\r\n.\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->send(std::move(out)); + + email->m_status = EMail::Quit; + } else if (email->m_status == EMail::Quit && responseCode == "250") { + trantor::MsgBuffer out; + std::string outMsg; + + outMsg.append("QUIT"); + outMsg.append("\r\n"); + + out.append(outMsg.data(), outMsg.size()); + + connPtr->send(std::move(out)); + + email->m_status = EMail::Close; + } else if (email->m_status == EMail::Close) { + /*Callback here for succeed delivery is probable*/ + cb("EMail sent. ID : " + email->m_uuid); + return; + } else { + email->m_status = EMail::Close; + /*Callback here for notification is probable*/ + cb(receivedMsg); + } +} + +std::string +SMTPMail::sendEmail(const std::string &mailServer, const uint16_t &port, + const std::string &from, const std::string &to, + const std::string &subject, const std::string &content, + const std::string &user, const std::string &passwd, + bool isHTML, + const std::function &cb) { + if (mailServer.empty() || from.empty() || to.empty() || subject.empty() || + user.empty() || passwd.empty()) { + LOG_WARN << "Invalid input(s) - " + << "\nServer : " << mailServer << "\nPort : " << port + << "\nfrom : " << from << "\nto : " << to + << "\nsubject : " << subject << "\nuser : " << user + << "\npasswd : " << passwd; + return {}; + } + + static auto hasLineBreak = [](const std::string &msg) { + if (std::string::npos != msg.find_first_of("\n") || + std::string::npos != msg.find_first_of("\r")) { + return true; + } + return false; + }; + + if (hasLineBreak(from)) { + LOG_WARN << "Invalid \"FROM\" data : " << from; + return {}; + } + if (hasLineBreak(to)) { + LOG_WARN << "Invalid \"TO\" data : " << to; + return {}; + } + if (hasLineBreak(subject)) { + LOG_WARN << "Invalid \"SUBJECT\" data : " << subject.data(); + return {}; + } + + LOG_TRACE << "New TcpClient : " << mailServer << ":" << port; + + // Create the email + auto email = std::make_shared(from, to, subject, content, user, passwd, + isHTML, nullptr); + + auto resolver = app().getResolver(); + resolver->resolve( + mailServer, [email, port, cb](const trantor::InetAddress &addr) { + constexpr size_t defaultLoopIdA = 10; + constexpr size_t defaultLoopIdB = 9; + auto loopA = app().getIOLoop(defaultLoopIdA); + auto loopB = app().getIOLoop(defaultLoopIdB); + + if ( loopA == loopB ) { + LOG_WARN << "Please provide at least 2 threads for this plugin"; + return; + } + + auto loop = loopA->isInLoopThread() ? loopB : loopA; + + assert(loop); // Should never be null + trantor::InetAddress addr_(addr.toIp(), port, false); + auto tcpSocket = + std::make_shared(loop, addr_, "SMTPMail"); + + email->m_socket = tcpSocket; + + std::weak_ptr email_wptr = email; + + EMail::m_emails.emplace(email->m_uuid, + email); // Assuming there is no uuid collision + tcpSocket->setConnectionCallback( + [email_wptr](const trantor::TcpConnectionPtr &connPtr) { + auto email_ptr = email_wptr.lock(); + if (!email_ptr) { + LOG_WARN << "EMail pointer gone"; + return; + } + if (connPtr->connected()) { + // send request; + LOG_TRACE << "Connection established!"; + } else { + LOG_TRACE << "Connection disconnect"; + EMail::m_emails.erase( + email_ptr->m_uuid); // Remove the email in list + // thisPtr->onError(std::string("ReqResult::NetworkFailure")); + } + }); + tcpSocket->setConnectionErrorCallback([email_wptr]() { + auto email_ptr = email_wptr.lock(); + if (!email_ptr) { + LOG_ERROR << "EMail pointer gone"; + return; + } + // can't connect to server + LOG_ERROR << "Bad Server address"; + EMail::m_emails.erase(email_ptr->m_uuid); // Remove the email in list + // thisPtr->onError(std::string("ReqResult::BadServerAddress")); + }); + auto cb_(cb ? cb : [](const std::string &msg) { + LOG_INFO << "Default email callback : " << msg; + }); + tcpSocket->setMessageCallback( + [email_wptr, cb_](const trantor::TcpConnectionPtr &connPtr, + trantor::MsgBuffer *msg) { + auto email_ptr = email_wptr.lock(); + if (!email_ptr) { + LOG_ERROR << "EMail pointer gone"; + return; + } + // email->m_socket->disconnect(); + messagesHandle(connPtr, msg, email_ptr, cb_); + }); + tcpSocket->connect(); // Start trying to send the email + }); + return email->m_uuid; +} diff --git a/backend/SMTPMail-drogon-master/SMTPMail.h b/backend/SMTPMail-drogon-master/SMTPMail.h new file mode 100644 index 0000000..2170bb4 --- /dev/null +++ b/backend/SMTPMail-drogon-master/SMTPMail.h @@ -0,0 +1,63 @@ +/** + * + * SMTPMail.h + * + * This plugin is for SMTP mail delievery for the Drogon web-framework. +Implementation + * reference from the project "SMTPClient" with Qt5 by kelvins. Please check out + * https://github.com/kelvins/SMTPClient. + +Copyright 2020 ihmc3jn09hk + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + */ + +#pragma once + +#include + +class SMTPMail : public drogon::Plugin { +public: + SMTPMail() = default; + /// This method must be called by drogon to initialize and start the plugin. + /// It must be implemented by the user. + void initAndStart(const Json::Value &config) override; + + /// This method must be called by drogon to shutdown the plugin. + /// It must be implemented by the user. + void shutdown() override; + + /** Send an email + * return : An ID of the email. + */ + std::string sendEmail( + const std::string + &mailServer, // Mail server address/dns E.g. 127.0.0.1/smtp.mail.com + const uint16_t &port, // Port E.g. 587 + const std::string &from, // Send from whom E.g. drogon@gmail.com + const std::string &to, // Reciever E.g. drogon@yahoo.com + const std::string &subject, // The email title/subject + const std::string &content, // The email content. + const std::string &user, // User (Usually same as "from") + const std::string &passwd, // Password + bool isHTML, // content type + const std::function &cb = {} + // The callback for email sent notification + ); +}; diff --git a/backend/src/db/model/Inode.cc b/backend/model/Inode.cc similarity index 85% rename from backend/src/db/model/Inode.cc rename to backend/model/Inode.cc index 01d839e..5819a51 100644 --- a/backend/src/db/model/Inode.cc +++ b/backend/model/Inode.cc @@ -6,7 +6,7 @@ */ #include "Inode.h" -#include +#include "drogon/utils/Utilities.h" #include using namespace drogon; @@ -19,6 +19,7 @@ const std::string Inode::Cols::_name = "name"; const std::string Inode::Cols::_parent_id = "parent_id"; const std::string Inode::Cols::_owner_id = "owner_id"; const std::string Inode::Cols::_size = "size"; +const std::string Inode::Cols::_has_preview = "has_preview"; const std::string Inode::primaryKeyName = "id"; const bool Inode::hasPrimaryKey = true; const std::string Inode::tableName = "inode"; @@ -29,7 +30,8 @@ const std::vector Inode::metaData_={ {"name","std::string","text",0,0,0,0}, {"parent_id","uint64_t","integer",8,0,0,0}, {"owner_id","uint64_t","integer",8,0,0,1}, -{"size","uint64_t","integer",8,0,0,0} +{"size","uint64_t","integer",8,0,0,0}, +{"has_preview","uint64_t","integer",8,0,0,1} }; const std::string &Inode::getColumnName(size_t index) noexcept(false) { @@ -64,11 +66,15 @@ Inode::Inode(const Row &r, const ssize_t indexOffset) noexcept { size_=std::make_shared(r["size"].as()); } + if(!r["has_preview"].isNull()) + { + hasPreview_=std::make_shared(r["has_preview"].as()); + } } else { size_t offset = (size_t)indexOffset; - if(offset + 6 > r.size()) + if(offset + 7 > r.size()) { LOG_FATAL << "Invalid SQL result for this model"; return; @@ -104,13 +110,18 @@ Inode::Inode(const Row &r, const ssize_t indexOffset) noexcept { size_=std::make_shared(r[index].as()); } + index = offset + 6; + if(!r[index].isNull()) + { + hasPreview_=std::make_shared(r[index].as()); + } } } Inode::Inode(const Json::Value &pJson, const std::vector &pMasqueradingVector) noexcept(false) { - if(pMasqueradingVector.size() != 6) + if(pMasqueradingVector.size() != 7) { LOG_ERROR << "Bad masquerading vector"; return; @@ -163,6 +174,14 @@ Inode::Inode(const Json::Value &pJson, const std::vector &pMasquera size_=std::make_shared((uint64_t)pJson[pMasqueradingVector[5]].asUInt64()); } } + if(!pMasqueradingVector[6].empty() && pJson.isMember(pMasqueradingVector[6])) + { + dirtyFlag_[6] = true; + if(!pJson[pMasqueradingVector[6]].isNull()) + { + hasPreview_=std::make_shared((uint64_t)pJson[pMasqueradingVector[6]].asUInt64()); + } + } } Inode::Inode(const Json::Value &pJson) noexcept(false) @@ -215,12 +234,20 @@ Inode::Inode(const Json::Value &pJson) noexcept(false) size_=std::make_shared((uint64_t)pJson["size"].asUInt64()); } } + if(pJson.isMember("has_preview")) + { + dirtyFlag_[6]=true; + if(!pJson["has_preview"].isNull()) + { + hasPreview_=std::make_shared((uint64_t)pJson["has_preview"].asUInt64()); + } + } } void Inode::updateByMasqueradedJson(const Json::Value &pJson, const std::vector &pMasqueradingVector) noexcept(false) { - if(pMasqueradingVector.size() != 6) + if(pMasqueradingVector.size() != 7) { LOG_ERROR << "Bad masquerading vector"; return; @@ -272,6 +299,14 @@ void Inode::updateByMasqueradedJson(const Json::Value &pJson, size_=std::make_shared((uint64_t)pJson[pMasqueradingVector[5]].asUInt64()); } } + if(!pMasqueradingVector[6].empty() && pJson.isMember(pMasqueradingVector[6])) + { + dirtyFlag_[6] = true; + if(!pJson[pMasqueradingVector[6]].isNull()) + { + hasPreview_=std::make_shared((uint64_t)pJson[pMasqueradingVector[6]].asUInt64()); + } + } } void Inode::updateByJson(const Json::Value &pJson) noexcept(false) @@ -323,6 +358,14 @@ void Inode::updateByJson(const Json::Value &pJson) noexcept(false) size_=std::make_shared((uint64_t)pJson["size"].asUInt64()); } } + if(pJson.isMember("has_preview")) + { + dirtyFlag_[6] = true; + if(!pJson["has_preview"].isNull()) + { + hasPreview_=std::make_shared((uint64_t)pJson["has_preview"].asUInt64()); + } + } } const uint64_t &Inode::getValueOfId() const noexcept @@ -452,6 +495,23 @@ void Inode::setSizeToNull() noexcept dirtyFlag_[5] = true; } +const uint64_t &Inode::getValueOfHasPreview() const noexcept +{ + const static uint64_t defaultValue = uint64_t(); + if(hasPreview_) + return *hasPreview_; + return defaultValue; +} +const std::shared_ptr &Inode::getHasPreview() const noexcept +{ + return hasPreview_; +} +void Inode::setHasPreview(const uint64_t &pHasPreview) noexcept +{ + hasPreview_ = std::make_shared(pHasPreview); + dirtyFlag_[6] = true; +} + void Inode::updateId(const uint64_t id) { id_ = std::make_shared(id); @@ -464,7 +524,8 @@ const std::vector &Inode::insertColumns() noexcept "name", "parent_id", "owner_id", - "size" + "size", + "has_preview" }; return inCols; } @@ -526,6 +587,17 @@ void Inode::outputArgs(drogon::orm::internal::SqlBinder &binder) const binder << nullptr; } } + if(dirtyFlag_[6]) + { + if(getHasPreview()) + { + binder << getValueOfHasPreview(); + } + else + { + binder << nullptr; + } + } } const std::vector Inode::updateColumns() const @@ -551,6 +623,10 @@ const std::vector Inode::updateColumns() const { ret.push_back(getColumnName(5)); } + if(dirtyFlag_[6]) + { + ret.push_back(getColumnName(6)); + } return ret; } @@ -611,6 +687,17 @@ void Inode::updateArgs(drogon::orm::internal::SqlBinder &binder) const binder << nullptr; } } + if(dirtyFlag_[6]) + { + if(getHasPreview()) + { + binder << getValueOfHasPreview(); + } + else + { + binder << nullptr; + } + } } Json::Value Inode::toJson() const { @@ -663,6 +750,14 @@ Json::Value Inode::toJson() const { ret["size"]=Json::Value(); } + if(getHasPreview()) + { + ret["has_preview"]=(Json::UInt64)getValueOfHasPreview(); + } + else + { + ret["has_preview"]=Json::Value(); + } return ret; } @@ -670,7 +765,7 @@ Json::Value Inode::toMasqueradedJson( const std::vector &pMasqueradingVector) const { Json::Value ret; - if(pMasqueradingVector.size() == 6) + if(pMasqueradingVector.size() == 7) { if(!pMasqueradingVector[0].empty()) { @@ -738,6 +833,17 @@ Json::Value Inode::toMasqueradedJson( ret[pMasqueradingVector[5]]=Json::Value(); } } + if(!pMasqueradingVector[6].empty()) + { + if(getHasPreview()) + { + ret[pMasqueradingVector[6]]=(Json::UInt64)getValueOfHasPreview(); + } + else + { + ret[pMasqueradingVector[6]]=Json::Value(); + } + } return ret; } LOG_ERROR << "Masquerade failed"; @@ -789,6 +895,14 @@ Json::Value Inode::toMasqueradedJson( { ret["size"]=Json::Value(); } + if(getHasPreview()) + { + ret["has_preview"]=(Json::UInt64)getValueOfHasPreview(); + } + else + { + ret["has_preview"]=Json::Value(); + } return ret; } @@ -834,13 +948,23 @@ bool Inode::validateJsonForCreation(const Json::Value &pJson, std::string &err) if(!validJsonOfField(5, "size", pJson["size"], err, true)) return false; } + if(pJson.isMember("has_preview")) + { + if(!validJsonOfField(6, "has_preview", pJson["has_preview"], err, true)) + return false; + } + else + { + err="The has_preview column cannot be null"; + return false; + } return true; } bool Inode::validateMasqueradedJsonForCreation(const Json::Value &pJson, const std::vector &pMasqueradingVector, std::string &err) { - if(pMasqueradingVector.size() != 6) + if(pMasqueradingVector.size() != 7) { err = "Bad masquerading vector"; return false; @@ -904,6 +1028,19 @@ bool Inode::validateMasqueradedJsonForCreation(const Json::Value &pJson, return false; } } + if(!pMasqueradingVector[6].empty()) + { + if(pJson.isMember(pMasqueradingVector[6])) + { + if(!validJsonOfField(6, pMasqueradingVector[6], pJson[pMasqueradingVector[6]], err, true)) + return false; + } + else + { + err="The " + pMasqueradingVector[6] + " column cannot be null"; + return false; + } + } } catch(const Json::LogicError &e) { @@ -949,13 +1086,18 @@ bool Inode::validateJsonForUpdate(const Json::Value &pJson, std::string &err) if(!validJsonOfField(5, "size", pJson["size"], err, false)) return false; } + if(pJson.isMember("has_preview")) + { + if(!validJsonOfField(6, "has_preview", pJson["has_preview"], err, false)) + return false; + } return true; } bool Inode::validateMasqueradedJsonForUpdate(const Json::Value &pJson, const std::vector &pMasqueradingVector, std::string &err) { - if(pMasqueradingVector.size() != 6) + if(pMasqueradingVector.size() != 7) { err = "Bad masquerading vector"; return false; @@ -996,6 +1138,11 @@ bool Inode::validateMasqueradedJsonForUpdate(const Json::Value &pJson, if(!validJsonOfField(5, pMasqueradingVector[5], pJson[pMasqueradingVector[5]], err, false)) return false; } + if(!pMasqueradingVector[6].empty() && pJson.isMember(pMasqueradingVector[6])) + { + if(!validJsonOfField(6, pMasqueradingVector[6], pJson[pMasqueradingVector[6]], err, false)) + return false; + } } catch(const Json::LogicError &e) { @@ -1086,6 +1233,18 @@ bool Inode::validJsonOfField(size_t index, return false; } break; + case 6: + if(pJson.isNull()) + { + err="The " + fieldName + " column cannot be null"; + return false; + } + if(!pJson.isUInt64()) + { + err="Type error in the "+fieldName+" field"; + return false; + } + break; default: err="Internal error in the server"; return false; diff --git a/backend/src/db/model/Inode.h b/backend/model/Inode.h similarity index 89% rename from backend/src/db/model/Inode.h rename to backend/model/Inode.h index 2f59e33..684f179 100644 --- a/backend/src/db/model/Inode.h +++ b/backend/model/Inode.h @@ -6,17 +6,17 @@ */ #pragma once -#include -#include -#include -#include -#include +#include "drogon/orm/Result.h" +#include "drogon/orm/Row.h" +#include "drogon/orm/Field.h" +#include "drogon/orm/SqlBinder.h" +#include "drogon/orm/Mapper.h" #ifdef __cpp_impl_coroutine #include #endif -#include -#include -#include +#include "trantor/utils/Date.h" +#include "trantor/utils/Logger.h" +#include "json/json.h" #include #include #include @@ -48,6 +48,7 @@ class Inode static const std::string _parent_id; static const std::string _owner_id; static const std::string _size; + static const std::string _has_preview; }; const static int primaryKeyNumber; @@ -151,8 +152,16 @@ class Inode void setSize(const uint64_t &pSize) noexcept; void setSizeToNull() noexcept; + /** For column has_preview */ + ///Get the value of the column has_preview, returns the default value if the column is null + const uint64_t &getValueOfHasPreview() const noexcept; + ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null + const std::shared_ptr &getHasPreview() const noexcept; + ///Set the value of the column has_preview + void setHasPreview(const uint64_t &pHasPreview) noexcept; - static size_t getColumnNumber() noexcept { return 6; } + + static size_t getColumnNumber() noexcept { return 7; } static const std::string &getColumnName(size_t index) noexcept(false); Json::Value toJson() const; @@ -175,6 +184,7 @@ class Inode std::shared_ptr parentId_; std::shared_ptr ownerId_; std::shared_ptr size_; + std::shared_ptr hasPreview_; struct MetaData { const std::string colName_; @@ -186,7 +196,7 @@ class Inode const bool notNull_; }; static const std::vector metaData_; - bool dirtyFlag_[6]={ false }; + bool dirtyFlag_[7]={ false }; public: static const std::string &sqlForFindingByPrimaryKey() { @@ -229,6 +239,11 @@ class Inode sql += "size,"; ++parametersCount; } + if(dirtyFlag_[6]) + { + sql += "has_preview,"; + ++parametersCount; + } if(parametersCount > 0) { sql[sql.length()-1]=')'; @@ -261,6 +276,11 @@ class Inode { sql.append("?,"); + } + if(dirtyFlag_[6]) + { + sql.append("?,"); + } if(parametersCount > 0) { diff --git a/backend/src/db/model/Tokens.cc b/backend/model/Tokens.cc similarity index 99% rename from backend/src/db/model/Tokens.cc rename to backend/model/Tokens.cc index d02df5e..7b8311a 100644 --- a/backend/src/db/model/Tokens.cc +++ b/backend/model/Tokens.cc @@ -6,7 +6,7 @@ */ #include "Tokens.h" -#include +#include "drogon/utils/Utilities.h" #include using namespace drogon; diff --git a/backend/src/db/model/Tokens.h b/backend/model/Tokens.h similarity index 96% rename from backend/src/db/model/Tokens.h rename to backend/model/Tokens.h index b2fe9e2..a6c0da4 100644 --- a/backend/src/db/model/Tokens.h +++ b/backend/model/Tokens.h @@ -6,17 +6,17 @@ */ #pragma once -#include -#include -#include -#include -#include +#include "drogon/orm/Result.h" +#include "drogon/orm/Row.h" +#include "drogon/orm/Field.h" +#include "drogon/orm/SqlBinder.h" +#include "drogon/orm/Mapper.h" #ifdef __cpp_impl_coroutine #include #endif -#include -#include -#include +#include "trantor/utils/Date.h" +#include "trantor/utils/Logger.h" +#include "json/json.h" #include #include #include diff --git a/backend/src/db/model/User.cc b/backend/model/User.cc similarity index 99% rename from backend/src/db/model/User.cc rename to backend/model/User.cc index 45e3f9f..205b6fa 100644 --- a/backend/src/db/model/User.cc +++ b/backend/model/User.cc @@ -6,7 +6,7 @@ */ #include "User.h" -#include +#include "drogon/utils/Utilities.h" #include using namespace drogon; diff --git a/backend/src/db/model/User.h b/backend/model/User.h similarity index 98% rename from backend/src/db/model/User.h rename to backend/model/User.h index 8f13e8a..e5af610 100644 --- a/backend/src/db/model/User.h +++ b/backend/model/User.h @@ -6,17 +6,17 @@ */ #pragma once -#include -#include -#include -#include -#include +#include "drogon/orm/Result.h" +#include "drogon/orm/Row.h" +#include "drogon/orm/Field.h" +#include "drogon/orm/SqlBinder.h" +#include "drogon/orm/Mapper.h" #ifdef __cpp_impl_coroutine #include #endif -#include -#include -#include +#include "trantor/utils/Date.h" +#include "trantor/utils/Logger.h" +#include "json/json.h" #include #include #include diff --git a/backend/src/db/model/model.json b/backend/model/model.json similarity index 100% rename from backend/src/db/model/model.json rename to backend/model/model.json diff --git a/backend/shl/msd/blocking_iterator.hpp b/backend/shl/msd/blocking_iterator.hpp new file mode 100644 index 0000000..bec3c05 --- /dev/null +++ b/backend/shl/msd/blocking_iterator.hpp @@ -0,0 +1,68 @@ +// Copyright (C) 2022 Andrei Avram + +#ifndef MSD_CHANNEL_BLOCKING_ITERATOR_HPP_ +#define MSD_CHANNEL_BLOCKING_ITERATOR_HPP_ + +#include +#include + +namespace msd { + +/** + * @brief An iterator that block the current thread, + * waiting to fetch elements from the channel. + * + * Used to implement channel range-based for loop. + * + * @tparam Channel Instance of channel. + */ +template +class blocking_iterator { + public: + using value_type = typename channel::value_type; + + explicit blocking_iterator(channel& ch) : ch_{ch} {} + + /** + * Advances to next element in the channel. + */ + blocking_iterator operator++() const noexcept { return *this; } + + /** + * Returns an element from the channel. + */ + value_type operator*() const + { + value_type value; + value << ch_; + + return value; + } + + /** + * Makes iteration continue until the channel is closed and empty. + */ + bool operator!=(blocking_iterator) const + { + std::unique_lock lock{ch_.mtx_}; + ch_.waitBeforeRead(lock); + + return !(ch_.closed() && ch_.empty()); + } + + private: + channel& ch_; +}; + +} // namespace msd + +/** + * @brief Output iterator specialization + */ +template +struct std::iterator_traits> { + using value_type = typename msd::blocking_iterator::value_type; + using iterator_category = std::output_iterator_tag; +}; + +#endif // MSD_CHANNEL_BLOCKING_ITERATOR_HPP_ diff --git a/backend/shl/msd/channel.hpp b/backend/shl/msd/channel.hpp new file mode 100644 index 0000000..199004a --- /dev/null +++ b/backend/shl/msd/channel.hpp @@ -0,0 +1,130 @@ +// Copyright (C) 2022 Andrei Avram + +#ifndef MSD_CHANNEL_HPP_ +#define MSD_CHANNEL_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "blocking_iterator.hpp" + +namespace msd { + +#if (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)) +#define NODISCARD [[nodiscard]] +#else +#define NODISCARD +#endif + +namespace detail { +template +struct remove_cvref { + using type = typename std::remove_cv::type>::type; +}; + +template +using remove_cvref_t = typename remove_cvref::type; +} // namespace detail + +/** + * @brief Exception thrown if trying to write on closed channel. + */ +class closed_channel : public std::runtime_error { + public: + explicit closed_channel(const char* msg) : std::runtime_error{msg} {} +}; + +/** + * @brief Thread-safe container for sharing data between threads. + * + * Implements a blocking input iterator. + * + * @tparam T The type of the elements. + */ +template +class channel { + public: + using value_type = T; + using iterator = blocking_iterator>; + using size_type = std::size_t; + + /** + * Creates a new channel. + * + * @param capacity Number of elements the channel can store before blocking. + */ + explicit constexpr channel(size_type capacity = 0); + + /** + * Pushes an element into the channel. + * + * @throws closed_channel if channel is closed. + */ + template + friend void operator>>(Type&&, channel>&); + + /** + * Pops an element from the channel. + * + * @tparam Type The type of the elements + */ + template + friend void operator<<(Type&, channel&); + + /** + * Returns the number of elements in the channel. + */ + NODISCARD inline size_type constexpr size() const noexcept; + + /** + * Returns true if there are no elements in channel. + */ + NODISCARD inline bool constexpr empty() const noexcept; + + /** + * Closes the channel. + */ + inline void close() noexcept; + + /** + * Returns true if the channel is closed. + */ + NODISCARD inline bool closed() const noexcept; + + /** + * Iterator + */ + iterator begin() noexcept; + iterator end() noexcept; + + /** + * Channel cannot be copied or moved. + */ + channel(const channel&) = delete; + channel& operator=(const channel&) = delete; + channel(channel&&) = delete; + channel& operator=(channel&&) = delete; + virtual ~channel() = default; + + private: + const size_type cap_; + std::queue queue_; + std::mutex mtx_; + std::condition_variable cnd_; + std::atomic is_closed_{false}; + + inline void waitBeforeRead(std::unique_lock&); + friend class blocking_iterator; +}; + +#include "channel_impl.hpp" + +} // namespace msd + +#endif // MSD_CHANNEL_HPP_ diff --git a/backend/shl/msd/channel_impl.hpp b/backend/shl/msd/channel_impl.hpp new file mode 100644 index 0000000..1e4f4c3 --- /dev/null +++ b/backend/shl/msd/channel_impl.hpp @@ -0,0 +1,87 @@ +// Copyright (C) 2022 Andrei Avram + +template +constexpr channel::channel(const size_type capacity) : cap_{capacity} +{ +} + +template +void operator>>(T&& in, channel>& ch) +{ + if (ch.closed()) { + throw closed_channel{"cannot write on closed channel"}; + } + + std::unique_lock lock{ch.mtx_}; + + if (ch.cap_ > 0 && ch.queue_.size() == ch.cap_) { + ch.cnd_.wait(lock, [&ch]() { return ch.queue_.size() < ch.cap_; }); + } + + ch.queue_.push(std::forward(in)); + + ch.cnd_.notify_one(); +} + +template +void operator<<(T& out, channel& ch) +{ + if (ch.closed() && ch.empty()) { + return; + } + + { + std::unique_lock lock{ch.mtx_}; + ch.waitBeforeRead(lock); + + if (ch.queue_.size() > 0) { + out = std::move(ch.queue_.front()); + ch.queue_.pop(); + } + } + + ch.cnd_.notify_one(); +} + +template +constexpr typename channel::size_type channel::size() const noexcept +{ + return queue_.size(); +} + +template +constexpr bool channel::empty() const noexcept +{ + return queue_.empty(); +} + +template +void channel::close() noexcept +{ + is_closed_.store(true); + cnd_.notify_all(); +} + +template +bool channel::closed() const noexcept +{ + return is_closed_.load(); +} + +template +blocking_iterator> channel::begin() noexcept +{ + return blocking_iterator>{*this}; +} + +template +blocking_iterator> channel::end() noexcept +{ + return blocking_iterator>{*this}; +} + +template +void channel::waitBeforeRead(std::unique_lock& lock) +{ + cnd_.wait(lock, [this] { return queue_.size() > 0 || closed(); }); +} diff --git a/backend/src/controllers/admin.cpp b/backend/src/controllers/admin.cpp index ae777a1..87dbcbf 100644 --- a/backend/src/controllers/admin.cpp +++ b/backend/src/controllers/admin.cpp @@ -55,14 +55,15 @@ namespace api { void admin::delete_user(req_type req, cbk_type cbk) { Json::Value& json = *req->jsonObject(); + msd::channel chan; try { uint64_t user_id = dto::json_get(json, "user").value(); db::MapperUser user_mapper(drogon::app().getDbClient()); auto user = user_mapper.findByPrimaryKey(user_id); auth::revoke_all(user); - fs::delete_node(fs::get_node(user.getValueOfRootId()).value(), true); - user_mapper.deleteOne(user); + fs::delete_node(fs::get_node(user.getValueOfRootId()).value(), chan, true); + user_mapper.deleteOne(user); cbk(dto::Responses::get_success_res()); } catch (const std::exception&) { cbk(dto::Responses::get_badreq_res("Validation error")); diff --git a/backend/src/controllers/auth/auth_2fa.cpp b/backend/src/controllers/auth/auth_2fa.cpp index a8c067b..02123c8 100644 --- a/backend/src/controllers/auth/auth_2fa.cpp +++ b/backend/src/controllers/auth/auth_2fa.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include "controllers/controllers.h" #include "db/db.h" @@ -24,21 +24,14 @@ std::string create_totp_qrcode(const db::User& user, const std::string& b32_secr const int mod_count = code.getSize(); const int row_size = qrcode_pixel_size * mod_count; - std::vector secret, image, row; - row.reserve(row_size); - image.reserve(row_size * row_size); + cv::Mat image(mod_count, mod_count, CV_8UC1), scaled_image; + std::vector image_encoded; + for (int y = 0; y < mod_count; y++) for (int x = 0; x < mod_count; x++) + image.at(x, y) = code.getModule(x, y) ? 0 : 0xff; + cv::resize(image, scaled_image, cv::Size(), qrcode_pixel_size, qrcode_pixel_size, cv::INTER_NEAREST); + cv::imencode(".png", scaled_image, image_encoded); - for (int y = 0; y < mod_count; y++) { - row.clear(); - for (int x = 0; x < mod_count; x++) - row.insert(row.end(), qrcode_pixel_size, code.getModule(x, y) ? 0 : 0xff); - for (int i = 0; i < qrcode_pixel_size; i++) - image.insert(image.end(), row.begin(), row.end()); - } - - lodepng::encode(secret, image, row_size, row_size, LCT_GREY, 8); - - return "data:image/png;base64," + Botan::base64_encode(secret); + return "data:image/png;base64," + Botan::base64_encode(image_encoded); } namespace api { @@ -64,7 +57,7 @@ namespace api { std::string code = create_totp_qrcode(user, b32_secret); cbk(dto::Responses::get_tfa_setup_res(b32_secret, code)); } - } catch (const std::exception&) { + } catch (const std::exception& e) { cbk(dto::Responses::get_badreq_res("Validation error")); } } diff --git a/backend/src/controllers/auth/auth_common.cpp b/backend/src/controllers/auth/auth_common.cpp index 120c3fc..c580cac 100644 --- a/backend/src/controllers/auth/auth_common.cpp +++ b/backend/src/controllers/auth/auth_common.cpp @@ -17,7 +17,7 @@ #include #include -#include +#include #include "controllers/controllers.h" #include "db/db.h" @@ -43,15 +43,17 @@ namespace api { char totp[16]; std::snprintf(totp, 16, "%06d", Botan::TOTP(Botan::OctetString(totp_secret)).generate_totp(t)); - mailio::message msg; - msg.from(mailio::mail_address("Fileserver", "fileserver@mattv.de")); - msg.add_recipient(mailio::mail_address(user.getValueOfName(), user.getValueOfName())); - msg.subject("Subject: Fileserver - Email 2fa code"); - msg.content("Your code is: " + std::string(totp) +"\r\nIt is valid for 5 Minutes"); - - mailio::smtps conn("mail.mattv.de", 587); - conn.authenticate("no-reply@mattv.de", "noreplyLONGPASS123", mailio::smtps::auth_method_t::START_TLS); - conn.submit(msg); + drogon::app().getPlugin()->sendEmail( + "mail.mattv.de", + 587, + "fileserver@mattv.de", + user.getValueOfName(), + "MFileserver - Email 2fa code", + "Your code is: " + std::string(totp) +"\r\nIt is valid for 5 Minutes", + "no-reply@mattv.de", + "noreplyLONGPASS123", + false + ); } std::string auth::get_token(const db::User& user) { diff --git a/backend/src/controllers/controllers.h b/backend/src/controllers/controllers.h index c9e1ba7..890b06d 100644 --- a/backend/src/controllers/controllers.h +++ b/backend/src/controllers/controllers.h @@ -1,11 +1,11 @@ #ifndef BACKEND_CONTROLLERS_H #define BACKEND_CONTROLLERS_H -#include -#include -#include -#include #include +#include +#include +#include + #include "db/db.h" using req_type = const drogon::HttpRequestPtr&; @@ -86,14 +86,32 @@ public: METHOD_ADD(fs::create_node_req, "/createFile", drogon::Post, "Login"); METHOD_ADD(fs::delete_node_req, "/delete/{}", drogon::Post, "Login"); METHOD_ADD(fs::upload, "/upload/{}", drogon::Post, "Login"); + METHOD_ADD(fs::create_zip, "/create_zip", drogon::Post, "Login"); METHOD_ADD(fs::download, "/download", drogon::Post, "Login"); + METHOD_ADD(fs::download_multi, "/download_multi", drogon::Post, "Login"); + METHOD_ADD(fs::download_preview, "/download_preview/{}", drogon::Get, "Login"); + METHOD_ADD(fs::download_base64, "/download_base64/{}", drogon::Get, "Login"); + METHOD_ADD(fs::get_type, "/get_type/{}", drogon::Get, "Login"); METHOD_LIST_END + enum class create_node_error { + INVALID_NAME, + INVALID_PARENT, + FILE_PARENT + }; + + struct mutex_stream { + std::stringstream ss; + std::mutex mutex; + bool done = false; + }; + static std::optional get_node(uint64_t node); static std::optional get_node_and_validate(const db::User& user, uint64_t node); static std::vector get_children(const db::INode& parent); - 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, bool allow_root = false); + 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); void root(req_type, cbk_type); @@ -102,7 +120,12 @@ public: template void create_node_req(req_type req, cbk_type cbk); void delete_node_req(req_type, cbk_type, uint64_t node); void upload(req_type, cbk_type, uint64_t node); + void create_zip(req_type, cbk_type); void download(req_type, cbk_type); + void download_multi(req_type, cbk_type); + void download_preview(req_type, cbk_type, uint64_t node); + void download_base64(req_type, cbk_type, uint64_t node); + void get_type(req_type, cbk_type, uint64_t node); }; class user : public drogon::HttpController { diff --git a/backend/src/controllers/fs.cpp b/backend/src/controllers/fs.cpp index d6f5fb2..f16bb5b 100644 --- a/backend/src/controllers/fs.cpp +++ b/backend/src/controllers/fs.cpp @@ -3,12 +3,100 @@ #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<>:\"/\\|"; -std::string generate_path(db::INode node) { +// 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); @@ -16,14 +104,101 @@ std::string generate_path(db::INode node) { node = inode_mapper.findByPrimaryKey(node.getValueOfParentId()); path.push(node); } - std::stringstream ss; while (!path.empty()) { const db::INode& seg = path.top(); - ss << seg.getValueOfName(); - if (seg.getValueOfIsFile() == 0) ss << '/'; + str += seg.getValueOfName(); + if (seg.getValueOfIsFile() == 0) str += "/"; path.pop(); } - return ss.str(); +} + +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 { @@ -48,26 +223,31 @@ namespace api { 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) { + 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 {"Invalid 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 {"Invalid parent"}; + return {create_node_error::INVALID_PARENT}; if (parent_node->getValueOfIsFile() != 0) - return {"Can't use file as parent"}; + return {create_node_error::FILE_PARENT}; auto children = get_children(*parent_node); for (const auto& child : children) if (child.getValueOfName() == name) - return {"File/Folder already exists"}; + return {std::make_tuple( + child.getValueOfIsFile() != 0, + child.getValueOfId() + )}; node.setParentId(*parent); } db::MapperInode inode_mapper(drogon::app().getDbClient()); @@ -75,18 +255,56 @@ namespace api { return {node}; } - void fs::delete_node(db::INode node, bool allow_root) { + void fs::delete_node(db::INode node, msd::channel& chan, bool allow_root) { if (node.getValueOfParentId() == 0 && (!allow_root)) return; - if (node.getValueOfIsFile() == 0) { - auto children = get_children(node); - for (const auto& child : children) delete_node(child, false); - } else { + + 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(); } - db::MapperInode inode_mapper(drogon::app().getDbClient()); - inode_mapper.deleteOne(node); } void fs::root(req_type req, cbk_type cbk) { @@ -98,23 +316,11 @@ namespace api { 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->getValueOfIsFile() == 0) { - std::vector children; - for (const db::INode& child : get_children(*inode)) children.push_back(child.getValueOfId()); - cbk(dto::Responses::get_node_folder_res( - inode->getValueOfId(), - inode->getValueOfName(), - inode->getParentId(), - children - )); - } else - cbk(dto::Responses::get_node_file_res( - inode->getValueOfId(), - inode->getValueOfName(), - inode->getParentId(), - inode->getValueOfSize() - )); + 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) { @@ -122,8 +328,10 @@ namespace api { auto inode = get_node_and_validate(user, node); if (!inode.has_value()) cbk(dto::Responses::get_badreq_res("Unknown node")); - else - cbk(dto::Responses::get_path_res( generate_path(*inode))); + else { + auto path = generate_path(*inode); + cbk(dto::Responses::get_success_res(path)); + } } template @@ -135,10 +343,18 @@ namespace api { 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_badreq_res(std::get(new_node))); - else + 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")); } @@ -152,12 +368,27 @@ namespace api { else if (inode->getValueOfParentId() == 0) cbk(dto::Responses::get_badreq_res("Can't delete root")); else { - delete_node(*inode); - cbk(dto::Responses::get_success_res()); + 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); @@ -178,13 +409,73 @@ namespace api { 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); @@ -193,19 +484,114 @@ namespace api { cbk(dto::Responses::get_badreq_res("Invalid node")); return; } - auto inode = get_node_and_validate(user, *node_id); + 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::download_base64(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()); - cbk(drogon::HttpResponse::newFileResponse( - p.string(), - inode->getValueOfName() - )); + try { + std::string mime = mime_type_map.at(name.extension().string()); + std::ifstream file(p, std::ios::in | std::ios::binary); + std::vector content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + cbk(dto::Responses::get_download_base64_res("data:" + mime + ";base64," + Botan::base64_encode(content))); + } catch (const std::exception&) { + cbk(dto::Responses::get_badreq_res("Invalid file type")); + } + } + + 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 diff --git a/backend/src/controllers/user.cpp b/backend/src/controllers/user.cpp index aab0fb7..d650726 100644 --- a/backend/src/controllers/user.cpp +++ b/backend/src/controllers/user.cpp @@ -18,9 +18,10 @@ namespace api { void user::delete_user(req_type req, cbk_type cbk) { db::MapperUser user_mapper(drogon::app().getDbClient()); + msd::channel chan; db::User user = dto::get_user(req); auth::revoke_all(user); - fs::delete_node((fs::get_node(user.getValueOfRootId())).value(), true); + 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/db/db.h b/backend/src/db/db.h index f72e684..06e1165 100644 --- a/backend/src/db/db.h +++ b/backend/src/db/db.h @@ -6,9 +6,9 @@ #include #include -#include "model/Inode.h" -#include "model/Tokens.h" -#include "model/User.h" +#include "Inode.h" +#include "Tokens.h" +#include "User.h" const std::string jwt_secret = "CUM"; diff --git a/backend/src/dto/dto.h b/backend/src/dto/dto.h index 46ea9d9..d605993 100644 --- a/backend/src/dto/dto.h +++ b/backend/src/dto/dto.h @@ -30,6 +30,16 @@ namespace dto { db::UserRole role; }; + struct GetNodeEntry { + explicit GetNodeEntry(const db::INode& node) : id(node.getValueOfId()), name(node.getValueOfName()), is_file(node.getValueOfIsFile() != 0), has_preview(node.getValueOfHasPreview() != 0), parent(node.getParentId()) { + if (node.getValueOfIsFile() != 0) size = node.getValueOfSize(); + } + uint64_t id, size; + std::string name; + bool is_file, has_preview; + std::shared_ptr parent; + }; + drogon::HttpResponsePtr get_error_res(drogon::HttpStatusCode, const std::string &msg); drogon::HttpResponsePtr get_success_res(); drogon::HttpResponsePtr get_success_res(Json::Value &); @@ -46,10 +56,13 @@ namespace dto { drogon::HttpResponsePtr get_admin_users_res(const std::vector& users); drogon::HttpResponsePtr get_root_res(uint64_t root); - drogon::HttpResponsePtr get_node_folder_res(uint64_t id, const std::string& name, const std::shared_ptr& parent, const std::vector& children); - drogon::HttpResponsePtr get_node_file_res(uint64_t id, const std::string& name, const std::shared_ptr& parent, uint64_t size); - drogon::HttpResponsePtr get_path_res(const std::string& path); + drogon::HttpResponsePtr get_node_res(const GetNodeEntry& node, const std::vector& children); drogon::HttpResponsePtr get_new_node_res(uint64_t id); + drogon::HttpResponsePtr get_node_exists_res(uint64_t id, bool file); + drogon::HttpResponsePtr get_download_base64_res(const std::string& data); + drogon::HttpResponsePtr get_type_res(const std::string& type); + drogon::HttpResponsePtr get_create_zip_done_res(); + drogon::HttpResponsePtr get_create_zip_done_res(uint64_t progress, uint64_t total); } } diff --git a/backend/src/dto/responses.cpp b/backend/src/dto/responses.cpp index e0abff3..4a0db1f 100644 --- a/backend/src/dto/responses.cpp +++ b/backend/src/dto/responses.cpp @@ -63,30 +63,24 @@ namespace dto::Responses { return get_success_res(json); } - drogon::HttpResponsePtr get_node_folder_res(uint64_t id, const std::string &name, const std::shared_ptr &parent, const std::vector &children) { + Json::Value parse_node(const GetNodeEntry& node) { Json::Value json; - json["id"] = id; - json["name"] = name; - json["isFile"] = false; - json["parent"] = (parent != nullptr) ? *parent : Json::Value::nullSingleton(); - for (uint64_t child : children) - json["children"].append(child); - return get_success_res(json); + json["id"] = node.id; + json["name"] = node.name; + json["isFile"] = node.is_file; + json["preview"] = node.has_preview; + json["parent"] = (node.parent != nullptr) ? *node.parent : Json::Value::nullSingleton(); + if (node.is_file) json["size"] = node.size; + return json; } - drogon::HttpResponsePtr get_node_file_res(uint64_t id, const std::string &name, const std::shared_ptr &parent, uint64_t size) { - Json::Value json; - json["id"] = id; - json["name"] = name; - json["isFile"] = true; - json["parent"] = (parent != nullptr) ? *parent : Json::Value::nullSingleton(); - json["size"] = size; - return get_success_res(json); - } - - drogon::HttpResponsePtr get_path_res(const std::string& path) { - Json::Value json; - json["path"] = path; + drogon::HttpResponsePtr get_node_res(const GetNodeEntry& node, const std::vector& children) { + Json::Value json = parse_node(node); + if (!node.is_file) { + json["children"] = Json::Value(Json::arrayValue); + for (const GetNodeEntry& child : children) + json["children"].append(parse_node(child)); + } return get_success_res(json); } @@ -95,4 +89,38 @@ namespace dto::Responses { json["id"] = id; return get_success_res(json); } + + drogon::HttpResponsePtr get_node_exists_res(uint64_t id, bool file) { + Json::Value json; + json["id"] = id; + json["exists"] = true; + json["isFile"] = file; + return get_success_res(json); + } + + drogon::HttpResponsePtr get_download_base64_res(const std::string &data) { + Json::Value json; + json["data"] = data; + return get_success_res(json); + } + + drogon::HttpResponsePtr get_type_res(const std::string &type) { + Json::Value json; + json["type"] = type; + return get_success_res(json); + } + + drogon::HttpResponsePtr get_create_zip_done_res() { + Json::Value json; + json["done"] = true; + return get_success_res(json); + } + + drogon::HttpResponsePtr get_create_zip_done_res(uint64_t progress, uint64_t total) { + Json::Value json; + json["done"] = false; + json["progress"] = progress; + json["total"] = total; + return get_success_res(json); + } } diff --git a/backend/src/filters/filters.cpp b/backend/src/filters/filters.cpp index 471a36f..c6547a7 100644 --- a/backend/src/filters/filters.cpp +++ b/backend/src/filters/filters.cpp @@ -16,7 +16,7 @@ void cleanup_tokens(db::MapperToken& mapper) { void Login::doFilter(const drogon::HttpRequestPtr& req, drogon::FilterCallback&& cb, drogon::FilterChainCallback&& ccb) { std::string token_str; - if (req->path() == "/api/fs/download") { + if (req->path() == "/api/fs/download" || req->path() == "/api/fs/download_multi") { token_str = req->getParameter("jwtToken"); } else { std::string auth_header = req->getHeader("Authorization"); diff --git a/backend/src/main.cpp b/backend/src/main.cpp index 802b680..2f55a20 100644 --- a/backend/src/main.cpp +++ b/backend/src/main.cpp @@ -13,6 +13,9 @@ void cleanup() { std::cout << "Cleanup up uploads..."; std::filesystem::remove_all("uploads"); std::cout << " [Done]" << std::endl; + std::cout << "Removing temp folder..." << std::flush; + std::filesystem::remove_all("temp"); + std::cout << " [Done]" << std::endl; std::cout << "Goodbye!" << std::endl; } @@ -47,6 +50,15 @@ int main(int argc, char* argv[]) { std::filesystem::create_directory("logs"); std::cout << " [Done]" << std::endl; } + if (std::filesystem::exists("temp")) { + std::cout << "Removing existing temp folder..." << std::flush; + std::filesystem::remove_all("temp"); + std::cout << " [Done]" << std::endl; + } + std::cout << "Creating temp folder..." << std::flush; + std::filesystem::create_directory("temp"); + std::cout << " [Done]" << std::endl; + auto* loop = drogon::app().getLoop(); loop->queueInLoop([]{ @@ -76,7 +88,8 @@ int main(int argc, char* argv[]) { " 'name' TEXT,\n" " 'parent_id' INTEGER,\n" " 'owner_id' INTEGER NOT NULL,\n" - " 'size' INTEGER\n" + " 'size' INTEGER,\n" + " 'has_preview' INTEGER NOT NULL\n" ")"); std::cout << " [Done]" << std::endl; std::cout << "Started!" << std::endl; @@ -101,8 +114,12 @@ int main(int argc, char* argv[]) { Json::Value access_logger; access_logger["name"] = "drogon::plugin::AccessLogger"; + Json::Value smtp_mail; + smtp_mail["name"] = "SMTPMail"; + Json::Value config; config["plugins"].append(access_logger); + config["plugins"].append(smtp_mail); drogon::app() .setClientMaxBodySize(std::numeric_limits::max()) @@ -123,8 +140,10 @@ int main(int argc, char* argv[]) { .setIntSignalHandler(cleanup) .setTermSignalHandler(cleanup) - .addListener("0.0.0.0", 5678) - .setThreadNum(2); + .enableRelaunchOnError() + + .addListener("0.0.0.0", 2345) + .setThreadNum(8); std::cout << "Setup done!" << std::endl; drogon::app().run(); diff --git a/backend/vcpkg-configuration.json b/backend/vcpkg-configuration.json new file mode 100644 index 0000000..1d9709f --- /dev/null +++ b/backend/vcpkg-configuration.json @@ -0,0 +1,15 @@ +{ + "default-registry": { + "kind": "git", + "repository": "https://github.com/microsoft/vcpkg.git", + "baseline": "927006b24c3a28dfd8aa0ec5f8ce43098480a7f1" + }, + "registries": [ + { + "kind": "filesystem", + "baseline": "default", + "path": "./vcpkg_reg", + "packages": [ "drogon" ] + } + ] +} \ No newline at end of file diff --git a/backend/vcpkg.json b/backend/vcpkg.json index 310b235..d7e7dae 100644 --- a/backend/vcpkg.json +++ b/backend/vcpkg.json @@ -7,11 +7,15 @@ "name": "drogon", "features": ["orm", "sqlite3"] }, + { + "name": "opencv4", + "default-features": false, + "features": ["tiff", "png", "jpeg", "webp", "openexr"] + }, "jwt-cpp", "botan", - "mailio", "nayuki-qr-code-generator", - "lodepng", - "openssl" + "openssl", + "kubazip" ] } \ No newline at end of file diff --git a/backend/vcpkg_reg/ports/drogon/drogon_config.patch b/backend/vcpkg_reg/ports/drogon/drogon_config.patch new file mode 100644 index 0000000..61b7c96 --- /dev/null +++ b/backend/vcpkg_reg/ports/drogon/drogon_config.patch @@ -0,0 +1,13 @@ +diff --git a/cmake/templates/DrogonConfig.cmake.in b/cmake/templates/DrogonConfig.cmake.in +index a21122a..6367259 100644 +--- a/cmake/templates/DrogonConfig.cmake.in ++++ b/cmake/templates/DrogonConfig.cmake.in +@@ -19,7 +19,7 @@ find_dependency(UUID REQUIRED) + endif(NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND NOT WIN32) + find_dependency(ZLIB REQUIRED) + if(@pg_FOUND@) +-find_dependency(pg) ++find_dependency(PostgreSQL) + endif() + if(@SQLite3_FOUND@) + find_dependency(SQLite3) diff --git a/backend/vcpkg_reg/ports/drogon/portfile.cmake b/backend/vcpkg_reg/ports/drogon/portfile.cmake new file mode 100644 index 0000000..e639187 --- /dev/null +++ b/backend/vcpkg_reg/ports/drogon/portfile.cmake @@ -0,0 +1,61 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO an-tao/drogon + REF v1.8.0 + SHA512 a834d937e3719059223d9bf19d777dbc92eaf09c5c9c44b5a742bfefcbcd95a146a6568cef8c058050fb87e330f221434ffe784dfa29a49de12b031f86ab1a33 + HEAD_REF master + PATCHES + vcpkg.patch + drogon_config.patch +) + +vcpkg_check_features( + OUT_FEATURE_OPTIONS FEATURE_OPTIONS + FEATURES + ctl BUILD_CTL + mysql BUILD_MYSQL + orm BUILD_ORM + postgres BUILD_POSTGRESQL + postgres LIBPQ_BATCH_MODE + redis BUILD_REDIS + sqlite3 BUILD_SQLITE +) + +string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "dynamic" BUILD_DROGON_SHARED) + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + DISABLE_PARALLEL_CONFIGURE + OPTIONS + -DBUILD_SHARED_LIBS=${BUILD_DROGON_SHARED} + -DBUILD_EXAMPLES=OFF + -DCMAKE_DISABLE_FIND_PACKAGE_Boost=ON + ${FEATURE_OPTIONS} + MAYBE_UNUSED_VARIABLES + CMAKE_DISABLE_FIND_PACKAGE_Boost +) + +vcpkg_cmake_install(ADD_BIN_TO_PATH) + +# Fix CMake files +vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/Drogon) + +vcpkg_fixup_pkgconfig() + +# Copy drogon_ctl +if("ctl" IN_LIST FEATURES) + vcpkg_copy_tools(TOOL_NAMES drogon_ctl AUTO_CLEAN) +endif() + +# Remove includes in debug +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") +if(VCPKG_LIBRARY_LINKAGE STREQUAL "static") + file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/bin" "${CURRENT_PACKAGES_DIR}/debug/bin") +endif() + +file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") +file(INSTALL "${SOURCE_PATH}/LICENSE" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright) + +# Copy pdb files +vcpkg_copy_pdbs() diff --git a/backend/vcpkg_reg/ports/drogon/usage b/backend/vcpkg_reg/ports/drogon/usage new file mode 100644 index 0000000..7887e34 --- /dev/null +++ b/backend/vcpkg_reg/ports/drogon/usage @@ -0,0 +1,4 @@ +The package drogon provides CMake targets: + + find_package(Drogon CONFIG REQUIRED) + target_link_libraries(main PRIVATE Drogon::Drogon) diff --git a/backend/vcpkg_reg/ports/drogon/vcpkg.json b/backend/vcpkg_reg/ports/drogon/vcpkg.json new file mode 100644 index 0000000..a5fd728 --- /dev/null +++ b/backend/vcpkg_reg/ports/drogon/vcpkg.json @@ -0,0 +1,92 @@ +{ + "name": "drogon", + "version-semver": "1.8.0", + "description": "A C++14/17 based HTTP web application framework running on Linux/macOS/Unix/Windows", + "homepage": "https://github.com/an-tao/drogon", + "documentation": "https://drogon.docsforge.com/master/overview/", + "license": "MIT", + "dependencies": [ + "brotli", + "jsoncpp", + { + "name": "libuuid", + "platform": "!windows & !osx" + }, + "trantor", + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + }, + "zlib" + ], + "features": { + "ctl": { + "description": "Build drogon_ctl tool." + }, + "mysql": { + "description": "Support reading and writing from/to MySQL databases.", + "dependencies": [ + { + "name": "drogon", + "features": [ + "orm" + ] + }, + { + "name": "libmariadb", + "features": [ + "iconv" + ], + "platform": "osx" + }, + { + "name": "libmariadb", + "platform": "!osx" + } + ] + }, + "orm": { + "description": "Build with object-relational mapping support." + }, + "postgres": { + "description": "Support reading and writing from/to Postgres databases.", + "dependencies": [ + { + "name": "drogon", + "features": [ + "orm" + ] + }, + "libpq" + ] + }, + "redis": { + "description": "Support reading and writing from/to Redis databases.", + "dependencies": [ + { + "name": "drogon", + "features": [ + "orm" + ] + }, + "hiredis" + ] + }, + "sqlite3": { + "description": "Support reading and writing from/to SQLite databases.", + "dependencies": [ + { + "name": "drogon", + "features": [ + "orm" + ] + }, + "sqlite3" + ] + } + } +} diff --git a/backend/vcpkg_reg/ports/drogon/vcpkg.patch b/backend/vcpkg_reg/ports/drogon/vcpkg.patch new file mode 100644 index 0000000..326fcaa --- /dev/null +++ b/backend/vcpkg_reg/ports/drogon/vcpkg.patch @@ -0,0 +1,53 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -120,9 +120,9 @@ if (WIN32) + PRIVATE $) + endif (WIN32) + +-add_subdirectory(trantor) ++find_package(Trantor CONFIG REQUIRED) + +-target_link_libraries(${PROJECT_NAME} PUBLIC trantor) ++target_link_libraries(${PROJECT_NAME} PUBLIC Trantor::Trantor) + + if(${CMAKE_SYSTEM_NAME} STREQUAL "Haiku") + target_link_libraries(${PROJECT_NAME} PRIVATE network) +@@ -316,11 +316,10 @@ endif (NOT WIN32) + + if (BUILD_POSTGRESQL) + # find postgres +- find_package(pg) +- if (pg_FOUND) +- message(STATUS "libpq inc path:" ${PG_INCLUDE_DIRS}) +- message(STATUS "libpq lib:" ${PG_LIBRARIES}) +- target_link_libraries(${PROJECT_NAME} PRIVATE pg_lib) ++ find_package(PostgreSQL REQUIRED) ++ if(PostgreSQL_FOUND) ++ set(pg_FOUND true) ++ target_link_libraries(${PROJECT_NAME} PRIVATE PostgreSQL::PostgreSQL) + set(DROGON_SOURCES + ${DROGON_SOURCES} + orm_lib/src/postgresql_impl/PostgreSQLResultImpl.cc) +@@ -348,7 +348,7 @@ if (BUILD_POSTGRESQL) + ${private_headers} + orm_lib/src/postgresql_impl/PgConnection.h) + endif (libpq_supports_batch) +- endif (pg_FOUND) ++ endif (PostgreSQL_FOUND) + endif (BUILD_POSTGRESQL) + + if (BUILD_MYSQL) +diff --git a/drogon_ctl/CMakeLists.txt b/drogon_ctl/CMakeLists.txt +index 9f2f1e7..09871f8 100755 +--- a/drogon_ctl/CMakeLists.txt ++++ b/drogon_ctl/CMakeLists.txt +@@ -19,7 +19,7 @@ add_executable(_drogon_ctl + target_link_libraries(_drogon_ctl ${PROJECT_NAME}) + if (WIN32 AND BUILD_SHARED_LIBS) + set(DROGON_FILE $) +- set(TRANTOR_FILE $) ++ set(TRANTOR_FILE $) + add_custom_command(TARGET _drogon_ctl POST_BUILD + COMMAND ${CMAKE_COMMAND} + -DCTL_FILE=${DROGON_FILE} diff --git a/backend/vcpkg_reg/versions/baseline.json b/backend/vcpkg_reg/versions/baseline.json new file mode 100644 index 0000000..4e0ed56 --- /dev/null +++ b/backend/vcpkg_reg/versions/baseline.json @@ -0,0 +1,8 @@ +{ + "default": { + "drogon": { + "baseline": "1.8.0", + "port-version": 0 + } + } +} \ No newline at end of file diff --git a/backend/vcpkg_reg/versions/d-/drogon.json b/backend/vcpkg_reg/versions/d-/drogon.json new file mode 100644 index 0000000..ac6b835 --- /dev/null +++ b/backend/vcpkg_reg/versions/d-/drogon.json @@ -0,0 +1,9 @@ +{ + "versions": [ + { + "version-semver": "1.8.0", + "port-version": 0, + "path": "$/ports/drogon" + } + ] +} diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 0116d96..80bc732 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -1,15 +1,15 @@ /* eslint-env node */ -require("@rushstack/eslint-patch/modern-module-resolution"); +require('@rushstack/eslint-patch/modern-module-resolution'); module.exports = { - root: true, - extends: [ - "plugin:vue/vue3-essential", - "eslint:recommended", - "@vue/eslint-config-typescript/recommended", - "@vue/eslint-config-prettier", - ], - parserOptions: { - ecmaVersion: "latest", - }, + root: true, + extends: [ + 'plugin:vue/vue3-essential', + 'eslint:recommended', + '@vue/eslint-config-typescript/recommended', + '@vue/eslint-config-prettier' + ], + parserOptions: { + ecmaVersion: 'latest' + } }; diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..25b37d9 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "endOfLine": "lf" +} diff --git a/frontend/index.html b/frontend/index.html index 11603f8..c2925c7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,9 @@ - + - Vite App + MFileserver
diff --git a/frontend/package.json b/frontend/package.json index d528880..cdce3ff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,16 +4,16 @@ "private": true, "license": "suck my dick", "scripts": { - "dev": "vite build --outDir ../run/static --watch", + "dev": "vite build -c vite.dev.config.ts --mode development", "build": "run-p type-check build-only", "build-only": "vite build", "type-check": "vue-tsc --noEmit", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" }, "dependencies": { + "@vicons/carbon": "^0.12.0", + "@vicons/ionicons5": "^0.12.0", "axios": "^0.27.2", - "class-transformer": "^0.5.1", - "class-validator": "^0.13.2", "filesize": "^9.0.11", "jwt-decode": "^3.1.2", "naive-ui": "^2.32.1", @@ -26,6 +26,7 @@ "@rushstack/eslint-patch": "^1.1.4", "@types/node": "^18.7.14", "@vitejs/plugin-vue": "^3.0.1", + "@vitejs/plugin-vue-jsx": "^2.0.0", "@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-typescript": "^11.0.0", "@vue/tsconfig": "^0.1.3", diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico deleted file mode 100644 index df36fcf..0000000 Binary files a/frontend/public/favicon.ico and /dev/null differ diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..2b53a6f --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 838bc94..ce980a3 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,62 +1,113 @@ diff --git a/frontend/src/AppAsyncWrapper.vue b/frontend/src/AppAsyncWrapper.vue index cd21f6d..70054f7 100644 --- a/frontend/src/AppAsyncWrapper.vue +++ b/frontend/src/AppAsyncWrapper.vue @@ -1,12 +1,17 @@ diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index fb7aad7..8773dc5 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -1,54 +1,55 @@ -import { Requests, Responses, UserRole, get_token, post_token } from "./base"; +import type { Requests, Responses } from '@/dto'; +import { UserRole, get_token, post_token } from './base'; -export const get_users = (token: string): Promise => - get_token("/api/admin/users", token); +export const get_users = (token: string): Promise => + get_token('/api/admin/users', token); export const set_role = ( - user: number, - role: UserRole, - token: string -): Promise => - post_token( - "/api/admin/set_role", - { - user, - role, - }, - token - ); + user: number, + role: UserRole, + token: string +): Promise => + post_token( + '/api/admin/set_role', + { + user, + role + }, + token + ); export const logout = ( - user: number, - token: string -): Promise => - post_token( - "/api/admin/logout", - { - user, - }, - token - ); + user: number, + token: string +): Promise => + post_token( + '/api/admin/logout', + { + user + }, + token + ); export const delete_user = ( - user: number, - token: string -): Promise => - post_token( - "/api/admin/delete", - { - user, - }, - token - ); + user: number, + token: string +): Promise => + post_token( + '/api/admin/delete', + { + user + }, + token + ); export const disable_tfa = ( - user: number, - token: string -): Promise => - post_token( - "/api/admin/disable_2fa", - { - user, - }, - token - ); + user: number, + token: string +): Promise => + post_token( + '/api/admin/disable_2fa', + { + user + }, + token + ); diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index c1f3b1b..2750c68 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -1,93 +1,86 @@ -import { Responses, Requests, post, post_token } from "./base"; +import type { Requests, Responses } from '@/dto'; +import { post, post_token } from './base'; export const auth_login = ( - username: string, - password: string, - otp?: string -): Promise< - | Responses.Auth.LoginResponse - | Responses.Auth.TfaRequiredResponse - | Responses.ErrorResponse -> => - post("/api/auth/login", { - username: username, - password: password, - otp: otp, - }); + username: string, + password: string, + otp?: string +): Promise => + post('/api/auth/login', { + username: username, + password: password, + otp: otp + }); export const auth_signup = ( - username: string, - password: string -): Promise => - post("/api/auth/signup", { - username: username, - password: password, - }); + username: string, + password: string +): Promise => + post('/api/auth/signup', { + username: username, + password: password + }); export const refresh_token = ( - token: string -): Promise => - post_token("/api/auth/refresh", {}, token); + token: string +): Promise => + post_token('/api/auth/refresh', {}, token); export const change_password = ( - oldPw: string, - newPw: string, - token: string -): Promise => - post_token( - "/api/auth/change_password", - { - oldPassword: oldPw, - newPassword: newPw, - }, - token - ); + oldPw: string, + newPw: string, + token: string +): Promise => + post_token( + '/api/auth/change_password', + { + oldPassword: oldPw, + newPassword: newPw + }, + token + ); export const logout_all = ( - token: string -): Promise => - post_token("/api/auth/logout_all", {}, token); + token: string +): Promise => + post_token('/api/auth/logout_all', {}, token); export function tfa_setup( - mail: false, - token: string -): Promise; + mail: false, + token: string +): Promise; export function tfa_setup( - mail: true, - token: string -): Promise; + mail: true, + token: string +): Promise; export function tfa_setup( - mail: boolean, - token: string -): Promise< - | Responses.Auth.RequestEmailTfaResponse - | Responses.Auth.RequestTotpTfaResponse - | Responses.ErrorResponse -> { - return post_token( - "/api/auth/2fa/setup", - { - mail, - }, - token - ); + mail: boolean, + token: string +): Promise { + return post_token( + '/api/auth/2fa/setup', + { + mail + }, + token + ); } export const tfa_complete = ( - mail: boolean, - code: string, - token: string -): Promise => - post_token( - "/api/auth/2fa/complete", - { - mail, - code, - }, - token - ); + mail: boolean, + code: string, + token: string +): Promise => + post_token( + '/api/auth/2fa/complete', + { + mail, + code + }, + token + ); export const tfa_disable = ( - token: string -): Promise => - post_token("/api/auth/2fa/disable", {}, token); + token: string +): Promise => + post_token('/api/auth/2fa/disable', {}, token); diff --git a/frontend/src/api/base.ts b/frontend/src/api/base.ts index 54d91d9..d90f2df 100644 --- a/frontend/src/api/base.ts +++ b/frontend/src/api/base.ts @@ -1,62 +1,62 @@ -import axios from "axios"; -import { Requests, Responses, UserRole } from "../dto"; -export { Requests, Responses, UserRole }; +import axios from 'axios'; +import type { Requests, Responses, UploadFile } from '@/dto'; +import { UserRole } from '@/dto'; +export { Requests, Responses, UserRole, UploadFile }; -export const post = (url: string, data: T) => - axios - .post(url, data, { - headers: { "Content-type": "application/json" }, - }) - .then((res) => res.data) - .catch((err) => err.response.data); +export const post = (url: string, data: T) => + axios + .post(url, data, { + headers: { 'Content-type': 'application/json' } + }) + .then((res) => res.data) + .catch((err) => err.response.data); -export const post_token = ( - url: string, - data: T, - token: string +export const post_token = ( + url: string, + data: T, + token: string ) => - axios - .post(url, data, { - headers: { - Authorization: "Bearer " + token, - "Content-type": "application/json", - }, - }) - .then((res) => res.data) - .catch((err) => err.response.data); + axios + .post(url, data, { + headers: { + Authorization: 'Bearer ' + token, + 'Content-type': 'application/json' + } + }) + .then((res) => res.data) + .catch((err) => err.response.data); export const post_token_form = ( - url: string, - data: FormData, - token: string, - onProgress: (progressEvent: ProgressEvent) => void + url: string, + data: FormData, + token: string, + onProgress: (progressEvent: ProgressEvent) => void ) => - axios - .post(url, data, { - headers: { - Authorization: "Bearer " + token, - "Content-type": "multipart/form-data", - }, - onUploadProgress: onProgress, - }) - .then((res) => res.data) - .catch((err) => err.response.data); + axios + .post(url, data, { + headers: { + Authorization: 'Bearer ' + token, + 'Content-type': 'multipart/form-data' + }, + onUploadProgress: onProgress + }) + .then((res) => res.data) + .catch((err) => err.response.data); // eslint-disable-next-line @typescript-eslint/no-unused-vars export const get = (url: string) => - axios - .get(url) - .then((res) => res.data) - .catch((err) => err.response.data); + axios + .get(url) + .then((res) => res.data) + .catch((err) => err.response.data); export const get_token = (url: string, token: string) => - axios - .get(url, { - headers: { Authorization: "Bearer " + token }, - }) - .then((res) => res.data) - .catch((err) => err.response.data); + axios + .get(url, { + headers: { Authorization: 'Bearer ' + token } + }) + .then((res) => res.data) + .catch((err) => err.response.data); -export const isErrorResponse = ( - res: Responses.BaseResponse -): res is Responses.ErrorResponse => res.statusCode != 200; +export const isErrorResponse = (res: Responses.Base): res is Responses.Error => + res.statusCode > 299; diff --git a/frontend/src/api/fs.ts b/frontend/src/api/fs.ts index 9c16066..6f3ffac 100644 --- a/frontend/src/api/fs.ts +++ b/frontend/src/api/fs.ts @@ -1,84 +1,130 @@ +import type { Requests, Responses, UploadFile } from '@/dto'; import { - Responses, - Requests, - get_token, - post_token, - post_token_form, - isErrorResponse, -} from "./base"; + get_token, + post_token, + post_token_form, + isErrorResponse +} from './base'; export const get_root = ( - token: string -): Promise => - get_token("/api/fs/root", token); + token: string +): Promise => + get_token('/api/fs/root', token); export const get_node = ( - token: string, - node: number -): Promise => - get_token(`/api/fs/node/${node}`, token); + token: string, + node: number +): Promise => + get_token(`/api/fs/node/${node}`, token); export const get_path = ( - token: string, - node: number -): Promise => - get_token(`/api/fs/path/${node}`, token); + token: string, + node: number +): Promise => + get_token(`/api/fs/path/${node}`, token); export const create_folder = ( - token: string, - parent: number, - name: string -): Promise => - post_token( - "/api/fs/createFolder", - { - parent: parent, - name: name, - }, - token - ); + token: string, + parent: number, + name: string +): Promise< + Responses.CreateFolder | Responses.CreateFolderExists | Responses.Error +> => + post_token( + '/api/fs/createFolder', + { + parent: parent, + name: name + }, + token + ); export const create_file = ( - token: string, - parent: number, - name: string -): Promise => - post_token( - "/api/fs/createFile", - { - parent: parent, - name: name, - }, - token - ); + token: string, + parent: number, + name: string +): Promise< + Responses.CreateFolder | Responses.CreateFolderExists | Responses.Error +> => + post_token( + '/api/fs/createFile', + { + parent: parent, + name: name + }, + token + ); -export const delete_node = ( - token: string, - node: number -): Promise => - post_token(`/api/fs/delete/${node}`, {}, token); +export const create_zip = ( + token: string, + nodes: number[] +): Promise => + post_token( + '/api/fs/create_zip', + { + nodes: nodes + }, + token + ); -export const upload_file = async ( - token: string, - parent: number, - file: File, - onProgress: (progressEvent: ProgressEvent) => void -): Promise => { - const node = await create_file(token, parent, file.name); - if (isErrorResponse(node)) return node; +export const download_preview = ( + token: string, + node: number +): Promise => + get_token(`/api/fs/download_preview/${node}`, token); - const form = new FormData(); - form.set("file", file); - return post_token_form(`/api/fs/upload/${node.id}`, form, token, onProgress); -}; +export const download_base64 = ( + token: string, + node: number +): Promise => + get_token(`/api/fs/download_base64/${node}`, token); + +export const get_type = ( + token: string, + node: number +): Promise => + get_token(`/api/fs/get_type/${node}`, token); + +export async function upload_file( + token: string, + file: UploadFile, + onProgress: (progressEvent: ProgressEvent) => void +): Promise { + const node = await create_file(token, file.parent, file.file.name); + if (isErrorResponse(node)) return node; + if ('exists' in node && !node.isFile) + return { statusCode: 400, message: 'File exists as folder' }; + + const form = new FormData(); + form.set('file', file.file); + return post_token_form( + `/api/fs/upload/${node.id}`, + form, + token, + onProgress + ); +} export function download_file(token: string, id: number) { - const form = document.createElement("form"); - form.method = "post"; - form.target = "_blank"; - form.action = "/api/fs/download"; - form.innerHTML = ``; - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); + const form = document.createElement('form'); + form.method = 'post'; + form.target = '_blank'; + form.action = '/api/fs/download'; + form.innerHTML = ``; + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); +} + +export function download_multi_file(token: string, ids: number[]) { + const form = document.createElement('form'); + form.method = 'post'; + form.target = '_blank'; + form.action = '/api/fs/download_multi'; + form.innerHTML = ``; + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index b54dd28..b2f7c29 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,6 +1,7 @@ -export { Requests, Responses, UserRole, isErrorResponse } from "./base"; -export * as Auth from "./auth"; -export * as FS from "./fs"; -export * as User from "./user"; -export * as Admin from "./admin"; -export * from "./util"; +export type { Requests, Responses, UploadFile } from './base'; +export { UserRole, isErrorResponse } from './base'; +export * as Auth from './auth'; +export * as FS from './fs'; +export * as User from './user'; +export * as Admin from './admin'; +export * from './util'; diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index 670e55f..1a435fc 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -1,11 +1,12 @@ -import { Responses, get_token, post_token } from "@/api/base"; +import type { Responses } from '@/api/base'; +import { get_token, post_token } from '@/api/base'; export const get_user_info = ( - token: string -): Promise => - get_token("/api/user/info", token); + token: string +): Promise => + get_token('/api/user/info', token); export const delete_user = ( - token: string -): Promise => - post_token("/api/user/delete", {}, token); + token: string +): Promise => + post_token('/api/user/delete', {}, token); diff --git a/frontend/src/api/util.ts b/frontend/src/api/util.ts index dfd6518..7e15900 100644 --- a/frontend/src/api/util.ts +++ b/frontend/src/api/util.ts @@ -1,26 +1,26 @@ -import type { JwtPayload } from "jwt-decode"; -import type { Ref, UnwrapRef } from "vue"; -import jwtDecode from "jwt-decode"; -import { isErrorResponse } from "./base"; -import { refresh_token } from "./auth"; +import type { JwtPayload } from 'jwt-decode'; +import type { Ref, UnwrapRef } from 'vue'; +import jwtDecode from 'jwt-decode'; +import { isErrorResponse } from './base'; +import { refresh_token } from './auth'; export async function check_token( - token: TokenInjectType + token: TokenInjectType ): Promise { - if (!token.jwt.value) return token.logout(); - const payload = jwtDecode(token.jwt.value); - if (!payload) return token.logout(); - // Expires in more than 60 Minute - if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60)) - return token.jwt.value; - const new_token = await refresh_token(token.jwt.value); - if (isErrorResponse(new_token)) return token.logout(); - token.setToken(new_token.jwt); - return new_token.jwt; + if (!token.jwt.value) return token.logout(); + const payload = jwtDecode(token.jwt.value); + if (!payload) return token.logout(); + // Expires in more than 60 Minute + if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60)) + return token.jwt.value; + const new_token = await refresh_token(token.jwt.value); + if (isErrorResponse(new_token)) return token.logout(); + token.setToken(new_token.jwt); + return new_token.jwt; } export type TokenInjectType = { - jwt: Ref>; - setToken: (token: string) => void; - logout: () => void; + jwt: Ref>; + setToken: (token: string) => void; + logout: () => void; }; diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg deleted file mode 100644 index bc826fe..0000000 --- a/frontend/src/assets/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/AsyncImage.vue b/frontend/src/components/AsyncImage.vue new file mode 100644 index 0000000..8e6b7f5 --- /dev/null +++ b/frontend/src/components/AsyncImage.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/frontend/src/components/DirViewer/CreateZipDialog.tsx b/frontend/src/components/DirViewer/CreateZipDialog.tsx new file mode 100644 index 0000000..cc211a2 --- /dev/null +++ b/frontend/src/components/DirViewer/CreateZipDialog.tsx @@ -0,0 +1,90 @@ +import type { TokenInjectType } from '@/api'; +import { ref } from 'vue'; +import { NProgress, NButton, NIcon } from 'naive-ui'; +import filesize from 'filesize'; +import { Archive, Download } from '@vicons/carbon'; +import { FS, check_token, isErrorResponse } from '@/api'; +import type { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider'; + +export default function createZipDialog( + nodes: number[], + dialog: DialogApiInjection, + jwt: TokenInjectType +) { + const progress = ref(0); + const total = ref(1); + const percentage = ref(0); + const done = ref(false); + const dia = dialog.create({ + title: 'Create Archive...', + closable: false, + closeOnEsc: false, + maskClosable: false, + icon: () => , + content: () => ( + + ), + action: () => + done.value ? ( + { + const token = await check_token(jwt); + if (!token) return; + if (nodes.length == 1) + FS.download_file(token, nodes[0]); + else FS.download_multi_file(token, nodes); + dia.destroy(); + }} + > + {{ + icon: () => ( + + + + ), + default: () => 'Download archive' + }} + + ) : ( +
+ {filesize(progress.value, { + base: 2, + standard: 'jedec' + })} + / + {filesize(total.value, { + base: 2, + standard: 'jedec' + })} + - {Math.floor(percentage.value * 1000) / 1000}% +
+ ) + }); + let updateRunning = false; + const updateInterval = setInterval(async () => { + if (updateRunning) return; + updateRunning = true; + const token = await check_token(jwt); + if (!token) return; + const resp = await FS.create_zip(token, nodes); + if (isErrorResponse(resp)) return; + if (resp.done) { + percentage.value = 100; + clearInterval(updateInterval); + done.value = true; + } else { + progress.value = resp.progress ?? 0; + total.value = resp.total ?? 1; + if (total.value == 0) total.value = 1; + percentage.value = (progress.value / total.value) * 100; + } + updateRunning = false; + }, 500); + return dia; +} diff --git a/frontend/src/components/DirViewer/DeleteModal.vue b/frontend/src/components/DirViewer/DeleteModal.vue new file mode 100644 index 0000000..a271513 --- /dev/null +++ b/frontend/src/components/DirViewer/DeleteModal.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/frontend/src/components/DirViewer/DirViewer.vue b/frontend/src/components/DirViewer/DirViewer.vue new file mode 100644 index 0000000..b8e036f --- /dev/null +++ b/frontend/src/components/DirViewer/DirViewer.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/frontend/src/components/DirViewer/DirViewerTable.vue b/frontend/src/components/DirViewer/DirViewerTable.vue new file mode 100644 index 0000000..3949100 --- /dev/null +++ b/frontend/src/components/DirViewer/DirViewerTable.vue @@ -0,0 +1,379 @@ + + + + + diff --git a/frontend/src/components/FSView/DirEntry.vue b/frontend/src/components/FSView/DirEntry.vue deleted file mode 100644 index d781c4f..0000000 --- a/frontend/src/components/FSView/DirEntry.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/frontend/src/components/FSView/DirViewer.vue b/frontend/src/components/FSView/DirViewer.vue deleted file mode 100644 index ac8614f..0000000 --- a/frontend/src/components/FSView/DirViewer.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - - - diff --git a/frontend/src/components/FSView/FileViewer.vue b/frontend/src/components/FSView/FileViewer.vue deleted file mode 100644 index 2428889..0000000 --- a/frontend/src/components/FSView/FileViewer.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - diff --git a/frontend/src/components/FileViewer/AudioVideoDownload.tsx b/frontend/src/components/FileViewer/AudioVideoDownload.tsx new file mode 100644 index 0000000..ddd30c1 --- /dev/null +++ b/frontend/src/components/FileViewer/AudioVideoDownload.tsx @@ -0,0 +1,45 @@ +import { ref } from 'vue'; +import { NProgress } from 'naive-ui'; +import filesize from 'filesize'; +import { Music, Video } from '@vicons/carbon'; +import type { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider'; + +export default function createAudioVideoDialog( + dialog: DialogApiInjection, + video: boolean +) { + const progress = ref(0); + const total = ref(1); + const percentage = ref(0); + const dia = dialog.create({ + title: video ? 'Loading video...' : 'Loading audio...', + closable: false, + closeOnEsc: false, + maskClosable: false, + icon: () => (video ?