blob: 65305a4197fa2a83ed7d45449f628804c6369d47 [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 "db-helper.hpp"
#include "logging.hpp"
#include <boost/bind.hpp>
#include <QDirIterator>
#include <QRegExp>
using namespace std;
using namespace boost;
_LOG_INIT(FsWatcher);
FsWatcher::FsWatcher(QString dirPath, LocalFile_Change_Callback onChange,
LocalFile_Change_Callback onDelete, QObject* parent)
: QObject(parent)
, m_watcher(new QFileSystemWatcher())
, m_scheduler(new Scheduler())
, 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)));
m_scheduler->start();
Scheduler::scheduleOneTimeTask(m_scheduler, 0,
bind(&FsWatcher::ScanDirectory_NotifyRemovals_Execute, this,
m_dirPath),
"rescan-r-" +
m_dirPath.toStdString()); // only one task will be scheduled per directory
Scheduler::scheduleOneTimeTask(m_scheduler, 0, bind(&FsWatcher::ScanDirectory_NotifyUpdates_Execute,
this, m_dirPath),
"rescan-" +
m_dirPath.toStdString()); // only one task will be scheduled per directory
}
FsWatcher::~FsWatcher()
{
m_scheduler->shutdown();
sqlite3_close(m_db);
}
void
FsWatcher::DidDirectoryChanged(QString dirPath)
{
_LOG_DEBUG("Triggered DirPath: " << dirPath.toStdString());
filesystem::path absPathTriggeredDir(dirPath.toStdString());
if (!filesystem::exists(filesystem::path(absPathTriggeredDir))) {
Scheduler::scheduleOneTimeTask(m_scheduler, 0.5,
bind(&FsWatcher::ScanDirectory_NotifyRemovals_Execute, this,
dirPath),
"r-" + dirPath.toStdString()); // only one task will be scheduled per directory
}
else {
// m_executor.execute (bind (&FsWatcher::ScanDirectory_NotifyUpdates_Execute, this, dirPath));
Scheduler::scheduleOneTimeTask(m_scheduler, 0.5,
bind(&FsWatcher::ScanDirectory_NotifyUpdates_Execute, this,
dirPath),
dirPath.toStdString()); // only one task will be scheduled per directory
// m_executor.execute (bind (&FsWatcher::ScanDirectory_NotifyUpdates_Execute, this, dirPath));
Scheduler::scheduleOneTimeTask(m_scheduler, 300,
bind(&FsWatcher::ScanDirectory_NotifyUpdates_Execute, this,
dirPath),
"rescan-" +
dirPath.toStdString()); // only one task will be scheduled per directory
Scheduler::scheduleOneTimeTask(m_scheduler, 300,
bind(&FsWatcher::ScanDirectory_NotifyRemovals_Execute, this,
m_dirPath),
"rescan-r-" +
m_dirPath.toStdString()); // only one task will be scheduled per directory
}
}
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;
filesystem::path absPathTriggeredFile(filePath.toStdString());
filePath.remove(0, m_dirPath.size());
filesystem::path triggeredFile(filePath.toStdString());
if (filesystem::exists(filesystem::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);
Scheduler::scheduleOneTimeTask(m_scheduler, 0.5, bind(m_onChange, triggeredFile.relative_path()),
triggeredFile.relative_path().string());
}
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());
Scheduler::scheduleOneTimeTask(m_scheduler, 0.5, bind(m_onDelete, triggeredFile.relative_path()),
"r-" + triggeredFile.relative_path().string());
}
}
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());
filesystem::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());
filesystem::path absPathTriggeredDir(dirPath.toStdString());
dirPath.remove(0, m_dirPath.size());
filesystem::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 ++)
{
filesystem::path testFile = filesystem::path (m_dirPath.toStdString ()) / file->filename ();
_LOG_DEBUG ("Check file for deletion [" << testFile.generic_string () << "]");
if (!filesystem::exists (testFile))
{
if (removeIncomplete || file->is_complete ())
{
_LOG_DEBUG ("Notifying about removed file [" << file->filename () << "]");
m_onDelete (file->filename ());
}
}
}
*/
vector<string> files;
getFilesInDir(triggeredDir.relative_path(), files);
for (vector<string>::iterator file = files.begin(); file != files.end(); file++) {
filesystem::path targetFile = filesystem::path(m_dirPath.toStdString()) / *file;
if (!filesystem::exists(targetFile)) {
deleteFile(*file);
m_onDelete(*file);
}
}
}
const string INIT_DATABASE = "\
CREATE TABLE IF NOT EXISTS \n\
Files( \n\
filename TEXT NOT NULL, \n\
PRIMARY KEY (filename) \n\
); \n\
CREATE INDEX filename_index ON Files (filename); \n\
";
void
FsWatcher::initFileStateDb()
{
filesystem::path dbFolder =
filesystem::path(m_dirPath.toStdString()) / ".chronoshare" / "fs_watcher";
filesystem::create_directories(dbFolder);
int res = sqlite3_open((dbFolder / "filestate.db").string().c_str(), &m_db);
if (res != SQLITE_OK) {
BOOST_THROW_EXCEPTION(Error::Db() << errmsg_info_str("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);
cout << "FS-Watcher DB error: " << errmsg << endl;
sqlite3_free(errmsg);
}
}
bool
FsWatcher::fileExists(const filesystem::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 filesystem::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 filesystem::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 filesystem::path& dir, vector<string>& files)
{
sqlite3_stmt* stmt;
sqlite3_prepare_v2(m_db, "SELECT * FROM Files WHERE filename LIKE ?;", -1, &stmt, 0);
string dirStr = dir.string();
ostringstream escapedDir;
for (string::const_iterator ch = dirStr.begin(); ch != dirStr.end(); ch++) {
if (*ch == '%')
escapedDir << "\\%";
else
escapedDir << *ch;
}
escapedDir << "/"
<< "%";
string escapedDirStr = escapedDir.str();
sqlite3_bind_text(stmt, 1, escapedDirStr.c_str(), escapedDirStr.size(), SQLITE_STATIC);
while (sqlite3_step(stmt) == SQLITE_ROW) {
string filename(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0)),
sqlite3_column_bytes(stmt, 0));
files.push_back(filename);
}
sqlite3_finalize(stmt);
}
#if WAF
#include "fs-watcher.cc.moc"
#include "fs-watcher.moc"
#endif