| /* -*- 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 |