blob: b791e16f7eab228557124cc30791a33bdeda5fb4 [file] [log] [blame]
/* -*- 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));
}
}