| /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
| /** |
| * Copyright (c) 2013-2016, Regents of the University of California. |
| * |
| * This file is part of ChronoShare, a decentralized file sharing application over NDN. |
| * |
| * ChronoShare is free software: you can redistribute it and/or modify it under the terms |
| * of the GNU General Public License as published by the Free Software Foundation, either |
| * version 3 of the License, or (at your option) any later version. |
| * |
| * ChronoShare is distributed in the hope that it will be useful, but WITHOUT ANY |
| * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A |
| * PARTICULAR PURPOSE. See the GNU General Public License for more details. |
| * |
| * You should have received copies of the GNU General Public License along with |
| * ChronoShare, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>. |
| * |
| * See AUTHORS.md for complete list of ChronoShare authors and contributors. |
| */ |
| |
| #include "state-server.h" |
| #include "logging.h" |
| #include "periodic-task.h" |
| #include "simple-interval-generator.h" |
| #include "task.h" |
| #include <boost/date_time/posix_time/posix_time.hpp> |
| #include <boost/lexical_cast.hpp> |
| #include <boost/make_shared.hpp> |
| #include <utility> |
| |
| INIT_LOGGER("StateServer"); |
| |
| using namespace Ndnx; |
| using namespace std; |
| using namespace boost; |
| |
| StateServer::StateServer(CcnxWrapperPtr ccnx, ActionLogPtr actionLog, |
| const boost::filesystem::path& rootDir, const Ccnx::Name& userName, |
| const std::string& sharedFolderName, const std::string& appName, |
| ObjectManager& objectManager, int freshness /* = -1*/) |
| : m_ccnx(ccnx) |
| , m_actionLog(actionLog) |
| , m_objectManager(objectManager) |
| , m_rootDir(rootDir) |
| , m_freshness(freshness) |
| , m_executor(1) |
| , m_userName(userName) |
| , m_sharedFolderName(sharedFolderName) |
| , m_appName(appName) |
| { |
| // may be later /localhost should be replaced with /%C1.M.S.localhost |
| |
| // <PREFIX_INFO> = /localhost/<user's-device-name>/"chronoshare"/"info" |
| m_PREFIX_INFO = Name("/localhost")(m_userName)("chronoshare")(m_sharedFolderName)("info"); |
| |
| // <PREFIX_CMD> = /localhost/<user's-device-name>/"chronoshare"/"cmd" |
| m_PREFIX_CMD = Name("/localhost")(m_userName)("chronoshare")(m_sharedFolderName)("cmd"); |
| |
| m_executor.start(); |
| |
| registerPrefixes(); |
| } |
| |
| StateServer::~StateServer() |
| { |
| m_executor.shutdown(); |
| |
| deregisterPrefixes(); |
| } |
| |
| void |
| StateServer::registerPrefixes() |
| { |
| // currently supporting limited number of command. |
| // will be extended to support all planned commands later |
| |
| // <PREFIX_INFO>/"actions"/"all"/<segment> get list of all actions |
| m_ccnx->setInterestFilter(Name(m_PREFIX_INFO)("actions")("folder"), |
| bind(&StateServer::info_actions_folder, this, _1)); |
| m_ccnx->setInterestFilter(Name(m_PREFIX_INFO)("actions")("file"), |
| bind(&StateServer::info_actions_file, this, _1)); |
| |
| // <PREFIX_INFO>/"filestate"/"all"/<segment> |
| m_ccnx->setInterestFilter(Name(m_PREFIX_INFO)("files")("folder"), |
| bind(&StateServer::info_files_folder, this, _1)); |
| |
| // <PREFIX_CMD>/"restore"/"file"/<one-component-relative-file-name>/<version>/<file-hash> |
| m_ccnx->setInterestFilter(Name(m_PREFIX_CMD)("restore")("file"), |
| bind(&StateServer::cmd_restore_file, this, _1)); |
| } |
| |
| void |
| StateServer::deregisterPrefixes() |
| { |
| m_ccnx->clearInterestFilter(Name(m_PREFIX_INFO)("actions")("folder")); |
| m_ccnx->clearInterestFilter(Name(m_PREFIX_INFO)("actions")("file")); |
| m_ccnx->clearInterestFilter(Name(m_PREFIX_INFO)("files")("folder")); |
| m_ccnx->clearInterestFilter(Name(m_PREFIX_CMD)("restore")("file")); |
| } |
| |
| void |
| StateServer::formatActionJson(json_spirit::Array& actions, const Ccnx::Name& name, |
| sqlite3_int64 seq_no, const ActionItem& action) |
| { |
| /* |
| * { |
| * "id": { |
| * "userName": "<NDN-NAME-OF-THE-USER>", |
| * "seqNo": "<SEQ_NO_OF_THE_ACTION>" |
| * }, |
| * "timestamp": "<ACTION-TIMESTAMP>", |
| * "filename": "<FILENAME>", |
| * |
| * "action": "UPDATE | DELETE", |
| * |
| * // only if update |
| * "update": { |
| * "hash": "<FILE-HASH>", |
| * "timestamp": "<FILE-TIMESTAMP>", |
| * "chmod": "<FILE-MODE>", |
| * "segNum": "<NUMBER-OF-SEGMENTS (~file size)>" |
| * }, |
| * |
| * // if parent_device_name is set |
| * "parentId": { |
| * "userName": "<NDN-NAME-OF-THE-USER>", |
| * "seqNo": "<SEQ_NO_OF_THE_ACTION>" |
| * } |
| * } |
| */ |
| |
| using namespace json_spirit; |
| using namespace boost::posix_time; |
| |
| Object json; |
| Object id; |
| |
| id.push_back(Pair("userName", boost::lexical_cast<string>(name))); |
| id.push_back(Pair("seqNo", static_cast<int64_t>(seq_no))); |
| |
| json.push_back(Pair("id", id)); |
| |
| json.push_back(Pair("timestamp", to_iso_extended_string(from_time_t(action.timestamp())))); |
| json.push_back(Pair("filename", action.filename())); |
| json.push_back(Pair("version", action.version())); |
| json.push_back(Pair("action", (action.action() == 0) ? "UPDATE" : "DELETE")); |
| |
| if (action.action() == 0) { |
| Object update; |
| update.push_back(Pair("hash", boost::lexical_cast<string>( |
| Hash(action.file_hash().c_str(), action.file_hash().size())))); |
| update.push_back(Pair("timestamp", to_iso_extended_string(from_time_t(action.mtime())))); |
| |
| ostringstream chmod; |
| chmod << setbase(8) << setfill('0') << setw(4) << action.mode(); |
| update.push_back(Pair("chmod", chmod.str())); |
| |
| update.push_back(Pair("segNum", action.seg_num())); |
| json.push_back(Pair("update", update)); |
| } |
| |
| if (action.has_parent_device_name()) { |
| Object parentId; |
| Ccnx::Name parent_device_name(action.parent_device_name().c_str(), |
| action.parent_device_name().size()); |
| id.push_back(Pair("userName", boost::lexical_cast<string>(parent_device_name))); |
| id.push_back(Pair("seqNo", action.parent_seq_no())); |
| |
| json.push_back(Pair("parentId", parentId)); |
| } |
| |
| actions.push_back(json); |
| } |
| |
| void |
| StateServer::info_actions_folder(const Name& interest) |
| { |
| if (interest.size() - m_PREFIX_INFO.size() != 3 && interest.size() - m_PREFIX_INFO.size() != 4) { |
| _LOG_DEBUG("Invalid interest: " << interest); |
| return; |
| } |
| |
| _LOG_DEBUG(">> info_actions_folder: " << interest); |
| m_executor.execute(bind(&StateServer::info_actions_fileOrFolder_Execute, this, interest, true)); |
| } |
| |
| void |
| StateServer::info_actions_file(const Name& interest) |
| { |
| if (interest.size() - m_PREFIX_INFO.size() != 3 && interest.size() - m_PREFIX_INFO.size() != 4) { |
| _LOG_DEBUG("Invalid interest: " << interest); |
| return; |
| } |
| |
| _LOG_DEBUG(">> info_actions_file: " << interest); |
| m_executor.execute(bind(&StateServer::info_actions_fileOrFolder_Execute, this, interest, false)); |
| } |
| |
| |
| void |
| StateServer::info_actions_fileOrFolder_Execute(const Ccnx::Name& interest, bool isFolder /* = true*/) |
| { |
| // <PREFIX_INFO>/"actions"/"folder|file"/<folder|file>/<offset> get list of all actions |
| |
| try { |
| int offset = interest.getCompFromBackAsInt(0); |
| |
| /// @todo !!! add security checking |
| |
| string fileOrFolderName; |
| if (interest.size() - m_PREFIX_INFO.size() == 4) |
| fileOrFolderName = interest.getCompFromBackAsString(1); |
| else // == 3 |
| fileOrFolderName = ""; |
| /* |
| * { |
| * "actions": [ |
| * ... |
| * ], |
| * |
| * // only if there are more actions available |
| * "more": "<NDN-NAME-OF-NEXT-SEGMENT-OF-ACTION>" |
| * } |
| */ |
| |
| using namespace json_spirit; |
| Object json; |
| |
| Array actions; |
| bool more; |
| if (isFolder) { |
| more = |
| m_actionLog->LookupActionsInFolderRecursively(boost::bind(StateServer::formatActionJson, |
| boost::ref(actions), _1, _2, _3), |
| fileOrFolderName, offset * 10, 10); |
| } |
| else { |
| more = m_actionLog->LookupActionsForFile(boost::bind(StateServer::formatActionJson, |
| boost::ref(actions), _1, _2, _3), |
| fileOrFolderName, offset * 10, 10); |
| } |
| |
| json.push_back(Pair("actions", actions)); |
| |
| if (more) { |
| json.push_back(Pair("more", lexical_cast<string>(offset + 1))); |
| // Ccnx::Name more = Name (interest.getPartialName (0, interest.size () - 1))(offset + 1); |
| // json.push_back (Pair ("more", lexical_cast<string> (more))); |
| } |
| |
| ostringstream os; |
| write_stream(Value(json), os, pretty_print | raw_utf8); |
| m_ccnx->publishData(interest, os.str(), 1); |
| } |
| catch (Ccnx::NameException& ne) { |
| // ignore any unexpected interests and errors |
| _LOG_ERROR(*boost::get_error_info<Ccnx::error_info_str>(ne)); |
| } |
| } |
| |
| void |
| StateServer::formatFilestateJson(json_spirit::Array& files, const FileItem& file) |
| { |
| /** |
| * { |
| * "filestate": [ |
| * { |
| * "filename": "<FILENAME>", |
| * "owner": { |
| * "userName": "<NDN-NAME-OF-THE-USER>", |
| * "seqNo": "<SEQ_NO_OF_THE_ACTION>" |
| * }, |
| * |
| * "hash": "<FILE-HASH>", |
| * "timestamp": "<FILE-TIMESTAMP>", |
| * "chmod": "<FILE-MODE>", |
| * "segNum": "<NUMBER-OF-SEGMENTS (~file size)>" |
| * }, ..., |
| * ] |
| * |
| * // only if there are more actions available |
| * "more": "<NDN-NAME-OF-NEXT-SEGMENT-OF-FILESTATE>" |
| * } |
| */ |
| using namespace json_spirit; |
| using namespace boost::posix_time; |
| |
| Object json; |
| |
| json.push_back(Pair("filename", file.filename())); |
| json.push_back(Pair("version", file.version())); |
| { |
| Object owner; |
| Ccnx::Name device_name(file.device_name().c_str(), file.device_name().size()); |
| owner.push_back(Pair("userName", boost::lexical_cast<string>(device_name))); |
| owner.push_back(Pair("seqNo", file.seq_no())); |
| |
| json.push_back(Pair("owner", owner)); |
| } |
| |
| json.push_back(Pair("hash", boost::lexical_cast<string>( |
| Hash(file.file_hash().c_str(), file.file_hash().size())))); |
| json.push_back(Pair("timestamp", to_iso_extended_string(from_time_t(file.mtime())))); |
| |
| ostringstream chmod; |
| chmod << setbase(8) << setfill('0') << setw(4) << file.mode(); |
| json.push_back(Pair("chmod", chmod.str())); |
| |
| json.push_back(Pair("segNum", file.seg_num())); |
| |
| files.push_back(json); |
| } |
| |
| void |
| debugFileState(const FileItem& file) |
| { |
| std::cout << file.filename() << std::endl; |
| } |
| |
| void |
| StateServer::info_files_folder(const Ccnx::Name& interest) |
| { |
| if (interest.size() - m_PREFIX_INFO.size() != 3 && interest.size() - m_PREFIX_INFO.size() != 4) { |
| _LOG_DEBUG("Invalid interest: " << interest << ", " << interest.size() - m_PREFIX_INFO.size()); |
| return; |
| } |
| |
| _LOG_DEBUG(">> info_files_folder: " << interest); |
| m_executor.execute(bind(&StateServer::info_files_folder_Execute, this, interest)); |
| } |
| |
| |
| void |
| StateServer::info_files_folder_Execute(const Ccnx::Name& interest) |
| { |
| // <PREFIX_INFO>/"filestate"/"folder"/<one-component-relative-folder-name>/<offset> |
| try { |
| int offset = interest.getCompFromBackAsInt(0); |
| |
| // /// @todo !!! add security checking |
| |
| string folder; |
| if (interest.size() - m_PREFIX_INFO.size() == 4) |
| folder = interest.getCompFromBackAsString(1); |
| else // == 3 |
| folder = ""; |
| |
| /* |
| * { |
| * "files": [ |
| * ... |
| * ], |
| * |
| * // only if there are more actions available |
| * "more": "<NDN-NAME-OF-NEXT-SEGMENT-OF-ACTION>" |
| * } |
| */ |
| |
| using namespace json_spirit; |
| Object json; |
| |
| Array files; |
| bool more = m_actionLog->GetFileState() |
| ->LookupFilesInFolderRecursively(boost::bind(StateServer::formatFilestateJson, |
| boost::ref(files), _1), |
| folder, offset * 10, 10); |
| |
| json.push_back(Pair("files", files)); |
| |
| if (more) { |
| json.push_back(Pair("more", lexical_cast<string>(offset + 1))); |
| // Ccnx::Name more = Name (interest.getPartialName (0, interest.size () - 1))(offset + 1); |
| // json.push_back (Pair ("more", lexical_cast<string> (more))); |
| } |
| |
| ostringstream os; |
| write_stream(Value(json), os, pretty_print | raw_utf8); |
| m_ccnx->publishData(interest, os.str(), 1); |
| } |
| catch (Ccnx::NameException& ne) { |
| // ignore any unexpected interests and errors |
| _LOG_ERROR(*boost::get_error_info<Ccnx::error_info_str>(ne)); |
| } |
| } |
| |
| |
| void |
| StateServer::cmd_restore_file(const Ccnx::Name& interest) |
| { |
| if (interest.size() - m_PREFIX_CMD.size() != 4 && interest.size() - m_PREFIX_CMD.size() != 5) { |
| _LOG_DEBUG("Invalid interest: " << interest); |
| return; |
| } |
| |
| _LOG_DEBUG(">> cmd_restore_file: " << interest); |
| m_executor.execute(bind(&StateServer::cmd_restore_file_Execute, this, interest)); |
| } |
| |
| void |
| StateServer::cmd_restore_file_Execute(const Ccnx::Name& interest) |
| { |
| // <PREFIX_CMD>/"restore"/"file"/<one-component-relative-file-name>/<version>/<file-hash> |
| |
| /// @todo !!! add security checking |
| |
| try { |
| FileItemPtr file; |
| |
| if (interest.size() - m_PREFIX_CMD.size() == 5) { |
| Hash hash(head(interest.getCompFromBack(0)), interest.getCompFromBack(0).size()); |
| int64_t version = interest.getCompFromBackAsInt(1); |
| string filename = |
| interest.getCompFromBackAsString(2); // should be safe even with full relative path |
| |
| file = m_actionLog->LookupAction(filename, version, hash); |
| if (!file) { |
| _LOG_ERROR("Requested file is not found: [" << filename << "] version [" << version |
| << "] hash [" |
| << hash.shortHash() |
| << "]"); |
| } |
| } |
| else { |
| int64_t version = interest.getCompFromBackAsInt(0); |
| string filename = |
| interest.getCompFromBackAsString(1); // should be safe even with full relative path |
| |
| file = m_actionLog->LookupAction(filename, version, Hash(0, 0)); |
| if (!file) { |
| _LOG_ERROR("Requested file is not found: [" << filename << "] version [" << version << "]"); |
| } |
| } |
| |
| if (!file) { |
| m_ccnx->publishData(interest, "FAIL: Requested file is not found", 1); |
| return; |
| } |
| |
| Hash hash = Hash(file->file_hash().c_str(), file->file_hash().size()); |
| |
| /////////////////// |
| // now the magic // |
| /////////////////// |
| |
| boost::filesystem::path filePath = m_rootDir / file->filename(); |
| Name deviceName(file->device_name().c_str(), file->device_name().size()); |
| |
| try { |
| if (filesystem::exists(filePath) && filesystem::last_write_time(filePath) == file->mtime() && |
| #if BOOST_VERSION >= 104900 |
| filesystem::status(filePath).permissions() == static_cast<filesystem::perms>(file->mode()) && |
| #endif |
| *Hash::FromFileContent(filePath) == hash) { |
| m_ccnx->publishData(interest, "OK: File already exists", 1); |
| _LOG_DEBUG("Asking to assemble a file, but file already exists on a filesystem"); |
| return; |
| } |
| } |
| catch (filesystem::filesystem_error& error) { |
| m_ccnx->publishData(interest, "FAIL: File operation failed", 1); |
| _LOG_ERROR("File operations failed on [" << filePath << "] (ignoring)"); |
| } |
| |
| _LOG_TRACE("Restoring file [" << filePath << "]"); |
| if (m_objectManager.objectsToLocalFile(deviceName, hash, filePath)) { |
| last_write_time(filePath, file->mtime()); |
| #if BOOST_VERSION >= 104900 |
| permissions(filePath, static_cast<filesystem::perms>(file->mode())); |
| #endif |
| m_ccnx->publishData(interest, "OK", 1); |
| } |
| else { |
| m_ccnx->publishData(interest, "FAIL: Unknown error while restoring file", 1); |
| } |
| } |
| catch (Ccnx::NameException& ne) { |
| // ignore any unexpected interests and errors |
| _LOG_ERROR(*boost::get_error_info<Ccnx::error_info_str>(ne)); |
| } |
| } |