| /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
| /** |
| * Copyright (c) 2013-2017, 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.hpp" |
| #include "core/logging.hpp" |
| |
| #include <ndn-cxx/util/digest.hpp> |
| #include <ndn-cxx/util/string-helper.hpp> |
| |
| #include <boost/asio/io_service.hpp> |
| #include <boost/date_time/posix_time/posix_time.hpp> |
| #include <boost/filesystem/fstream.hpp> |
| #include <boost/lexical_cast.hpp> |
| |
| namespace ndn { |
| namespace chronoshare { |
| |
| _LOG_INIT(StateServer); |
| |
| namespace fs = boost::filesystem; |
| |
| StateServer::StateServer(Face& face, ActionLogPtr actionLog, const fs::path& rootDir, |
| const Name& userName, const std::string& sharedFolderName, |
| const name::Component& appName, ObjectManager& objectManager, |
| KeyChain& keyChain, time::milliseconds freshness) |
| : m_face(face) |
| , m_actionLog(actionLog) |
| , m_objectManager(objectManager) |
| , m_rootDir(rootDir) |
| , m_freshness(freshness) |
| , m_userName(userName) |
| , m_sharedFolderName(sharedFolderName) |
| , m_appName(appName) |
| , m_keyChain(keyChain) |
| , m_ioService(m_face.getIoService()) |
| { |
| // may be later /localhop should be replaced with /%C1.M.S.localhost |
| |
| // <PREFIX_INFO> = /localhop/<user's-device-name>/"chronoshare"/"info" |
| m_PREFIX_INFO = Name("/localhop"); |
| m_PREFIX_INFO.append(m_userName).append(m_appName).append(m_sharedFolderName).append("info"); |
| |
| // <PREFIX_CMD> = /localhop/<user's-device-name>/"chronoshare"/"cmd" |
| m_PREFIX_CMD = Name("/localhop"); |
| m_PREFIX_CMD.append(m_userName).append(m_appName).append(m_sharedFolderName).append("cmd"); |
| |
| registerPrefixes(); |
| } |
| |
| StateServer::~StateServer() |
| { |
| 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 |
| Name actionsFolder = Name(m_PREFIX_INFO); |
| actionsFolder.append("actions").append("folder"); |
| actionsFolderId = |
| m_face.setInterestFilter(InterestFilter(actionsFolder), |
| bind(&StateServer::info_actions_folder, this, _1, _2), |
| RegisterPrefixSuccessCallback(), RegisterPrefixFailureCallback()); |
| |
| _LOG_DEBUG("Register Prefix: " << actionsFolder); |
| |
| Name actionsFile = Name(m_PREFIX_INFO); |
| actionsFile.append("actions").append("file"); |
| actionsFileId = |
| m_face.setInterestFilter(InterestFilter(actionsFile), |
| bind(&StateServer::info_actions_file, this, _1, _2), |
| RegisterPrefixSuccessCallback(), RegisterPrefixFailureCallback()); |
| |
| _LOG_DEBUG("Register Prefix: " << actionsFile); |
| |
| // <PREFIX_INFO>/"filestate"/"all"/<segment> |
| Name filesFolder = Name(m_PREFIX_INFO); |
| filesFolder.append("files").append("folder"); |
| filesFolderId = |
| m_face.setInterestFilter(InterestFilter(filesFolder), |
| bind(&StateServer::info_files_folder, this, _1, _2), |
| RegisterPrefixSuccessCallback(), RegisterPrefixFailureCallback()); |
| |
| _LOG_DEBUG("Register Prefix: " << filesFolder); |
| |
| // <PREFIX_CMD>/"restore"/"file"/<one-component-relative-file-name>/<version>/<file-hash> |
| Name restoreFile = Name(m_PREFIX_CMD); |
| restoreFile.append("restore").append("file"); |
| restoreFileId = |
| m_face.setInterestFilter(InterestFilter(restoreFile), |
| bind(&StateServer::cmd_restore_file, this, _1, _2), |
| RegisterPrefixSuccessCallback(), RegisterPrefixFailureCallback()); |
| |
| _LOG_DEBUG("Register Prefix: " << restoreFile); |
| } |
| |
| void |
| StateServer::deregisterPrefixes() |
| { |
| m_face.unsetInterestFilter(actionsFolderId); |
| m_face.unsetInterestFilter(actionsFileId); |
| m_face.unsetInterestFilter(filesFolderId); |
| m_face.unsetInterestFilter(restoreFileId); |
| } |
| |
| void |
| StateServer::formatActionJson(json_spirit::Array& actions, const 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<std::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; |
| const Buffer hash(action.file_hash().c_str(), action.file_hash().size()); |
| update.push_back(Pair("hash", toHex(hash))); |
| update.push_back(Pair("timestamp", to_iso_extended_string(from_time_t(action.mtime())))); |
| |
| std::ostringstream chmod; |
| chmod << std::setbase(8) << std::setfill('0') << std::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; |
| Name parent_device_name(action.parent_device_name()); |
| id.push_back(Pair("userName", boost::lexical_cast<std::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 InterestFilter& interesFilter, const Interest& interestTrue) |
| { |
| Name interest = interestTrue.getName(); |
| _LOG_DEBUG(">> info_actions_folder: " << interest); |
| if (interest.size() - m_PREFIX_INFO.size() != 3 && interest.size() - m_PREFIX_INFO.size() != 4) { |
| _LOG_DEBUG("Invalid interest: " << interest); |
| return; |
| } |
| |
| m_ioService.post(bind(&StateServer::info_actions_fileOrFolder_Execute, this, interest, true)); |
| } |
| |
| void |
| StateServer::info_actions_file(const InterestFilter& interesFilter, const Interest& interestTrue) |
| { |
| Name interest = interestTrue.getName(); |
| 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_ioService.post(bind(&StateServer::info_actions_fileOrFolder_Execute, this, interest, false)); |
| } |
| |
| void |
| StateServer::info_actions_fileOrFolder_Execute(const Name& interest, bool isFolder /* = true*/) |
| { |
| // <PREFIX_INFO>/"actions"/"folder|file"/<folder|file>/<offset> get list of all actions |
| if (interest.size() < 1) { |
| // ignore any unexpected interests and errors |
| _LOG_ERROR("empty interest name"); |
| return; |
| } |
| uint64_t offset = interest.get(-1).toNumber(); |
| |
| /// @todo !!! add security checking |
| |
| std::string fileOrFolderName; |
| if (interest.size() - m_PREFIX_INFO.size() == 4) |
| fileOrFolderName = interest.get(-2).toUri(); |
| else // == 3 |
| fileOrFolderName = ""; |
| /* |
| * { |
| * "actions": [ |
| * ... |
| * ], |
| * |
| * // only if there are more actions available |
| * "more": "<NDN-NAME-OF-NEXT-SEGMENT-OF-ACTION>" |
| * } |
| */ |
| |
| _LOG_DEBUG("info_actions_fileOrFolder_Execute! offset: " << offset); |
| using namespace json_spirit; |
| Object json; |
| |
| Array actions; |
| bool more; |
| if (isFolder) { |
| more = m_actionLog->LookupActionsInFolderRecursively(bind(StateServer::formatActionJson, |
| boost::ref(actions), _1, _2, _3), |
| fileOrFolderName, offset * 10, 10); |
| } |
| else { |
| more = m_actionLog->LookupActionsForFile(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", boost::lexical_cast<std::string>(offset + 1))); |
| // Name more = Name(interest.getPartialName(0, interest.size() - 1))(offset + 1); |
| // json.push_back(Pair("more", boost::lexical_cast<std::string>(more))); |
| } |
| |
| std::ostringstream os; |
| write_stream(Value(json), os, pretty_print | raw_utf8); |
| |
| shared_ptr<Data> data = make_shared<Data>(); |
| data->setName(interest); |
| data->setFreshnessPeriod(m_freshness); |
| data->setContent(reinterpret_cast<const uint8_t*>(os.str().c_str()), os.str().size()); |
| m_keyChain.sign(*data); |
| m_face.put(*data); |
| } |
| |
| 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; |
| Name device_name(Block(file.device_name().data(), file.device_name().size())); |
| owner.push_back(Pair("userName", device_name.toUri())); |
| owner.push_back(Pair("seqNo", file.seq_no())); |
| |
| json.push_back(Pair("owner", owner)); |
| } |
| |
| json.push_back(Pair("hash", toHex(reinterpret_cast<const uint8_t*>(file.file_hash().data()), |
| file.file_hash().size()))); |
| json.push_back(Pair("timestamp", to_iso_extended_string(from_time_t(file.mtime())))); |
| |
| std::ostringstream chmod; |
| chmod << std::setbase(8) << std::setfill('0') << std::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 InterestFilter& interesFilter, const Interest& interestTrue) |
| { |
| Name interest = interestTrue.getName(); |
| 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_ioService.post(bind(&StateServer::info_files_folder_Execute, this, interest)); |
| } |
| |
| void |
| StateServer::info_files_folder_Execute(const Name& interest) |
| { |
| // <PREFIX_INFO>/"filestate"/"folder"/<one-component-relative-folder-name>/<offset> |
| if (interest.size() < 1) { |
| // ignore any unexpected interests and errors |
| _LOG_ERROR("empty interest name"); |
| return; |
| } |
| uint64_t offset = interest.get(-1).toNumber(); |
| |
| // /// @todo !!! add security checking |
| |
| std::string folder; |
| if (interest.size() - m_PREFIX_INFO.size() == 4) |
| folder = interest.get(-2).toUri(); |
| 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(bind(StateServer::formatFilestateJson, |
| boost::ref(files), _1), |
| folder, offset * 10, 10); |
| |
| json.push_back(Pair("files", files)); |
| |
| if (more) { |
| json.push_back(Pair("more", boost::lexical_cast<std::string>(offset + 1))); |
| // Name more = Name(interest.getPartialName(0, interest.size() - 1))(offset + 1); |
| // json.push_back(Pair("more", boost::lexical_cast<std::string>(more))); |
| } |
| |
| std::ostringstream os; |
| write_stream(Value(json), os, pretty_print | raw_utf8); |
| |
| shared_ptr<Data> data = make_shared<Data>(); |
| data->setName(interest); |
| data->setFreshnessPeriod(m_freshness); |
| data->setContent(reinterpret_cast<const uint8_t*>(os.str().c_str()), os.str().size()); |
| m_keyChain.sign(*data); |
| m_face.put(*data); |
| } |
| |
| void |
| StateServer::cmd_restore_file(const InterestFilter& interesFilter, const Interest& interestTrue) |
| { |
| Name interest = interestTrue.getName(); |
| 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_ioService.post(bind(&StateServer::cmd_restore_file_Execute, this, interest)); |
| } |
| |
| void |
| StateServer::cmd_restore_file_Execute(const Name& interest) |
| { |
| // <PREFIX_CMD>/"restore"/"file"/<one-component-relative-file-name>/<version>/<file-hash> |
| |
| /// @todo !!! add security checking |
| |
| FileItemPtr file; |
| |
| if (interest.size() - m_PREFIX_CMD.size() == 5) { |
| const Buffer hash(interest.get(-1).value(), interest.get(-1).value_size()); |
| uint64_t version = interest.get(-2).toNumber(); |
| std::string filename = interest.get(-3).toUri(); // should be safe even with full relative path |
| |
| _LOG_DEBUG("filename: " << filename << " version: " << version); |
| |
| file = m_actionLog->LookupAction(filename, version, hash); |
| |
| if (!file) { |
| _LOG_ERROR("Requested file is not found: [" << filename << "] version [" << version << "] hash [" |
| << toHex(hash) |
| << "]"); |
| } |
| } |
| else { |
| uint64_t version = interest.get(-1).toNumber(); |
| std::string filename = interest.get(-2).toUri(); |
| file = m_actionLog->LookupAction(filename, version, Buffer(0, 0)); |
| if (!file) { |
| _LOG_ERROR("Requested file is not found: [" << filename << "] version [" << version << "]"); |
| } |
| } |
| |
| if (!file) { |
| shared_ptr<Data> data = make_shared<Data>(); |
| data->setName(interest); |
| data->setFreshnessPeriod(m_freshness); |
| std::string msg = "FAIL: Requested file is not found"; |
| data->setContent(reinterpret_cast<const uint8_t*>(msg.c_str()), msg.size()); |
| m_keyChain.sign(*data); |
| m_face.put(*data); |
| return; |
| } |
| |
| const Buffer hash(file->file_hash().c_str(), file->file_hash().size()); |
| |
| /////////////////// |
| // now the magic // |
| /////////////////// |
| |
| fs::path filePath = m_rootDir / file->filename(); |
| Name deviceName( |
| Block((const unsigned char*)file->device_name().c_str(), file->device_name().size())); |
| |
| _LOG_DEBUG("filePath" << filePath << " deviceName " << deviceName); |
| |
| try { |
| if (fs::exists(filePath) && fs::last_write_time(filePath) == file->mtime() |
| #if BOOST_VERSION >= 104900 |
| && |
| fs::status(filePath).permissions() == static_cast<fs::perms>(file->mode()) |
| #endif |
| ) { |
| fs::ifstream input(filePath, std::ios::in | std::ios::binary); |
| if (*util::Sha256(input).computeDigest() == hash) { |
| shared_ptr<Data> data = make_shared<Data>(); |
| data->setName(interest); |
| data->setFreshnessPeriod(m_freshness); |
| std::string msg = "OK: File already exists"; |
| data->setContent(reinterpret_cast<const uint8_t*>(msg.c_str()), msg.size()); |
| m_keyChain.sign(*data); |
| m_face.put(*data); |
| _LOG_DEBUG("Asking to assemble a file, but file already exists on a filesystem"); |
| return; |
| } |
| } |
| } |
| catch (fs::filesystem_error& error) { |
| shared_ptr<Data> data = make_shared<Data>(); |
| data->setName(interest); |
| data->setFreshnessPeriod(m_freshness); |
| std::string msg = "FAIL: File operation failed"; |
| data->setContent(reinterpret_cast<const uint8_t*>(msg.c_str()), msg.size()); |
| m_keyChain.sign(*data); |
| m_face.put(*data); |
| _LOG_ERROR("File operations failed on [" << filePath << "](ignoring)"); |
| } |
| |
| _LOG_TRACE("Restoring file [" << filePath << "]" |
| << " deviceName " |
| << deviceName); |
| if (m_objectManager.objectsToLocalFile(deviceName, hash, filePath)) { |
| last_write_time(filePath, file->mtime()); |
| #if BOOST_VERSION >= 104900 |
| permissions(filePath, static_cast<fs::perms>(file->mode())); |
| #endif |
| shared_ptr<Data> data = make_shared<Data>(); |
| data->setName(interest); |
| data->setFreshnessPeriod(m_freshness); |
| std::string msg = "OK"; |
| data->setContent(reinterpret_cast<const uint8_t*>(msg.c_str()), msg.size()); |
| m_keyChain.sign(*data); |
| m_face.put(*data); |
| _LOG_DEBUG("Restoring file successfully!"); |
| } |
| else { |
| shared_ptr<Data> data = make_shared<Data>(); |
| data->setName(interest); |
| data->setFreshnessPeriod(m_freshness); |
| std::string msg = "FAIL: Unknown error while restoring file"; |
| data->setContent(reinterpret_cast<const uint8_t*>(msg.c_str()), msg.size()); |
| m_keyChain.sign(*data); |
| m_face.put(*data); |
| } |
| } |
| |
| } // namespace chronoshare |
| } // namespace ndn |