blob: 6e637cfa3e9890ac9528cdff7fffb30e892548c2 [file] [log] [blame]
/* -*- 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 "fs-watcher.hpp"
#include "core/logging.hpp"
#include <QDirIterator>
#include <QRegExp>
namespace ndn {
namespace chronoshare {
_LOG_INIT(FsWatcher);
namespace fs = boost::filesystem;
FsWatcher::FsWatcher(boost::asio::io_service& io, QString dirPath, LocalFile_Change_Callback onChange,
LocalFile_Change_Callback onDelete, QObject* parent)
: QObject(parent)
, m_watcher(new QFileSystemWatcher(this))
, m_scheduler(io)
, m_dirPath(dirPath)
, m_onChange(onChange)
, m_onDelete(onDelete)
{
_LOG_DEBUG("Monitor dir: " << m_dirPath.toStdString());
// add main directory to monitor
initFileStateDb();
m_watcher->addPath(m_dirPath);
// register signals(callback functions)
connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(DidDirectoryChanged(QString)));
connect(m_watcher, SIGNAL(fileChanged(QString)), this, SLOT(DidFileChanged(QString)));
rescheduleEvent("rescan", m_dirPath.toStdString(), time::seconds(0),
bind(&FsWatcher::ScanDirectory_NotifyUpdates_Execute, this, m_dirPath));
rescheduleEvent("rescan-r", m_dirPath.toStdString(), time::seconds(0),
bind(&FsWatcher::ScanDirectory_NotifyRemovals_Execute, this, m_dirPath));
}
FsWatcher::~FsWatcher()
{
sqlite3_close(m_db);
}
void
FsWatcher::rescheduleEvent(const std::string& eventType, const std::string& path,
const time::milliseconds& period, const Scheduler::Event& callback)
{
util::scheduler::ScopedEventId event(m_scheduler);
event = m_scheduler.scheduleEvent(period, callback);
// only one task per directory/file
std::string key = eventType + ":" + path;
m_events.erase(key);
m_events.insert(std::make_pair(key, std::move(event)));
}
void
FsWatcher::DidDirectoryChanged(QString dirPath)
{
_LOG_DEBUG("Triggered DirPath: " << dirPath.toStdString());
fs::path absPathTriggeredDir(dirPath.toStdString());
if (!fs::exists(fs::path(absPathTriggeredDir))) {
rescheduleEvent("r-", dirPath.toStdString(), time::milliseconds(500),
bind(&FsWatcher::ScanDirectory_NotifyRemovals_Execute, this, dirPath));
}
else {
rescheduleEvent("", dirPath.toStdString(), time::milliseconds(500),
bind(&FsWatcher::ScanDirectory_NotifyUpdates_Execute, this, dirPath));
// rescan updated folder for updates
rescheduleEvent("rescan", dirPath.toStdString(), time::seconds(300),
bind(&FsWatcher::ScanDirectory_NotifyUpdates_Execute, this, dirPath));
// rescan whole folder for deletions
rescheduleEvent("rescan-r", m_dirPath.toStdString(), time::seconds(300),
bind(&FsWatcher::ScanDirectory_NotifyRemovals_Execute, this, m_dirPath));
}
}
void
FsWatcher::DidFileChanged(QString filePath)
{
if (!filePath.startsWith(m_dirPath)) {
_LOG_ERROR(
"Got notification about a file not from the monitored directory: " << filePath.toStdString());
return;
}
QString absFilePath = filePath;
fs::path absPathTriggeredFile(filePath.toStdString());
filePath.remove(0, m_dirPath.size());
fs::path triggeredFile(filePath.toStdString());
if (fs::exists(fs::path(absPathTriggeredFile))) {
_LOG_DEBUG("Triggered UPDATE of file: " << triggeredFile.relative_path().generic_string());
// m_onChange(triggeredFile.relative_path());
m_watcher->removePath(absFilePath);
m_watcher->addPath(absFilePath);
rescheduleEvent("", triggeredFile.relative_path().string(), time::milliseconds(500),
bind(m_onChange, triggeredFile.relative_path()));
}
else {
_LOG_DEBUG("Triggered DELETE of file: " << triggeredFile.relative_path().generic_string());
// m_onDelete(triggeredFile.relative_path());
m_watcher->removePath(absFilePath);
deleteFile(triggeredFile.relative_path());
rescheduleEvent("r", triggeredFile.relative_path().string(), time::milliseconds(500),
bind(m_onDelete, triggeredFile.relative_path()));
}
}
void
FsWatcher::ScanDirectory_NotifyUpdates_Execute(QString dirPath)
{
_LOG_TRACE(" >> ScanDirectory_NotifyUpdates_Execute");
// exclude working only on last component, not the full path; iterating through all directories,
// even excluded from monitoring
QRegExp exclude("^(\\.|\\.\\.|\\.chronoshare|.*~|.*\\.swp)$");
QDirIterator dirIterator(dirPath, QDir::Dirs | QDir::Files | /*QDir::Hidden |*/ QDir::NoSymLinks |
QDir::NoDotAndDotDot,
QDirIterator::Subdirectories); // directory iterator(recursive)
// iterate through directory recursively
while (dirIterator.hasNext()) {
dirIterator.next();
// Get FileInfo
QFileInfo fileInfo = dirIterator.fileInfo();
QString name = fileInfo.fileName();
_LOG_DEBUG("+++ Scanning: " << name.toStdString());
if (!exclude.exactMatch(name)) {
_LOG_DEBUG("Not excluded file/dir: " << fileInfo.absoluteFilePath().toStdString());
QString absFilePath = fileInfo.absoluteFilePath();
// _LOG_DEBUG("Attempt to add path to watcher: " << absFilePath.toStdString());
m_watcher->removePath(absFilePath);
m_watcher->addPath(absFilePath);
if (fileInfo.isFile()) {
QString relFile = absFilePath;
relFile.remove(0, m_dirPath.size());
fs::path aFile(relFile.toStdString());
if (
//!m_fileState->LookupFile(aFile.relative_path().generic_string())
///* file does not exist there, but exists locally: added */)
!fileExists(aFile.relative_path()) /*file does not exist in db, but exists in fs: add */) {
addFile(aFile.relative_path());
DidFileChanged(absFilePath);
}
}
}
else {
// _LOG_DEBUG("Excluded file/dir: " << fileInfo.filePath().toStdString());
}
}
}
void
FsWatcher::ScanDirectory_NotifyRemovals_Execute(QString dirPath)
{
_LOG_DEBUG("Triggered DirPath: " << dirPath.toStdString());
fs::path absPathTriggeredDir(dirPath.toStdString());
dirPath.remove(0, m_dirPath.size());
fs::path triggeredDir(dirPath.toStdString());
/*
FileItemsPtr files = m_fileState->LookupFilesInFolderRecursively(triggeredDir.relative_path().generic_string());
for (std::list<FileItem>::iterator file = files->begin(); file != files->end(); file ++)
{
fs::path testFile = fs::path(m_dirPath.toStdString()) / file->filename();
_LOG_DEBUG("Check file for deletion [" << testFile.generic_string() << "]");
if (!fs::exists(testFile))
{
if (removeIncomplete || file->is_complete())
{
_LOG_DEBUG("Notifying about removed file [" << file->filename() << "]");
m_onDelete(file->filename());
}
}
}
*/
std::vector<std::string> files;
getFilesInDir(triggeredDir.relative_path(), files);
for (std::vector<std::string>::iterator file = files.begin(); file != files.end(); file++) {
fs::path targetFile = fs::path(m_dirPath.toStdString()) / *file;
if (!fs::exists(targetFile)) {
deleteFile(*file);
m_onDelete(*file);
}
}
}
const std::string INIT_DATABASE = "\
CREATE TABLE IF NOT EXISTS \n\
Files( \n\
filename TEXT NOT NULL, \n\
PRIMARY KEY(filename) \n\
); \n\
CREATE INDEX IF NOT EXISTS filename_index ON Files(filename); \n\
";
void
FsWatcher::initFileStateDb()
{
fs::path dbFolder = fs::path(m_dirPath.toStdString()) / ".chronoshare" / "fs_watcher";
fs::create_directories(dbFolder);
int res = sqlite3_open((dbFolder / "filestate.db").string().c_str(), &m_db);
if (res != SQLITE_OK) {
BOOST_THROW_EXCEPTION(Error("Cannot open database: " + (dbFolder / "filestate.db").string()));
}
char* errmsg = 0;
res = sqlite3_exec(m_db, INIT_DATABASE.c_str(), NULL, NULL, &errmsg);
if (res != SQLITE_OK && errmsg != 0) {
// _LOG_TRACE("Init \"error\": " << errmsg);
std::cout << "FS-Watcher DB error: " << errmsg << std::endl;
sqlite3_free(errmsg);
}
}
bool
FsWatcher::fileExists(const fs::path& filename)
{
sqlite3_stmt* stmt;
sqlite3_prepare_v2(m_db, "SELECT * FROM Files WHERE filename = ?;", -1, &stmt, 0);
sqlite3_bind_text(stmt, 1, filename.c_str(), -1, SQLITE_STATIC);
bool retval = false;
if (sqlite3_step(stmt) == SQLITE_ROW) {
retval = true;
}
sqlite3_finalize(stmt);
return retval;
}
void
FsWatcher::addFile(const fs::path& filename)
{
sqlite3_stmt* stmt;
sqlite3_prepare_v2(m_db, "INSERT OR IGNORE INTO Files(filename) VALUES(?);", -1, &stmt, 0);
sqlite3_bind_text(stmt, 1, filename.c_str(), -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
void
FsWatcher::deleteFile(const fs::path& filename)
{
sqlite3_stmt* stmt;
sqlite3_prepare_v2(m_db, "DELETE FROM Files WHERE filename = ?;", -1, &stmt, 0);
sqlite3_bind_text(stmt, 1, filename.c_str(), -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
void
FsWatcher::getFilesInDir(const fs::path& dir, std::vector<std::string>& files)
{
sqlite3_stmt* stmt;
sqlite3_prepare_v2(m_db, "SELECT * FROM Files WHERE filename LIKE ?;", -1, &stmt, 0);
std::string dirStr = dir.string();
std::ostringstream escapedDir;
for (std::string::const_iterator ch = dirStr.begin(); ch != dirStr.end(); ch++) {
if (*ch == '%')
escapedDir << "\\%";
else
escapedDir << *ch;
}
escapedDir << "/"
<< "%";
std::string escapedDirStr = escapedDir.str();
sqlite3_bind_text(stmt, 1, escapedDirStr.c_str(), escapedDirStr.size(), SQLITE_STATIC);
while (sqlite3_step(stmt) == SQLITE_ROW) {
std::string filename(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0)),
sqlite3_column_bytes(stmt, 0));
files.push_back(filename);
}
sqlite3_finalize(stmt);
}
} // namespace chronoshare
} // namespace ndn
#ifdef WAF
#include "fs-watcher.moc"
// #include "fs-watcher.cpp.moc"
#endif