Mercurial > minori
view src/core/filesystem.cc @ 398:650a9159a0e7
filesystem: implement win32 watchers
These are really nice :)
| author | Paper <paper@tflc.us> |
|---|---|
| date | Fri, 07 Nov 2025 14:32:11 -0500 |
| parents | 963047512d34 |
| children | a0bc3ae5164a |
line wrap: on
line source
#include "core/filesystem.h" #include "core/config.h" #include "core/strings.h" #include <QStandardPaths> #include <filesystem> #ifdef WIN32 # include <windows.h> #endif namespace Filesystem { /* this runs fs::create_directories() on the PARENT directory. */ void CreateDirectories(const std::filesystem::path &path) { if (path.empty()) return; const auto &parent = path.parent_path(); if (!std::filesystem::exists(parent)) std::filesystem::create_directories(parent); } std::filesystem::path GetDotPath() { /* * Windows: ~/AppData/Roaming/Minori * macOS: ~/Library/Application Support/Minori * ...: ~/.config/minori * * FIXME: are windows and mac properly cased? */ #ifdef WIN32 return Strings::ToUtf8String(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); #else return Strings::ToUtf8String(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); #endif } std::filesystem::path GetConfigPath() { return GetDotPath() / CONFIG_NAME; } std::filesystem::path GetAnimeDBPath() { return GetDotPath() / "anime" / "db.json"; } std::filesystem::path GetTorrentsPath() { return GetDotPath() / "torrents"; } std::filesystem::path GetAnimePostersPath() { return GetDotPath() / "anime" / "posters"; } /* ------------------------------------------------------------------------ */ /* Ehhhhh */ class Watcher : public IWatcher { public: Watcher(void *opaque, const std::filesystem::path &path, EventHandler handler) : path_(path), handler_(handler), opaque_(opaque) { } virtual ~Watcher() override {} protected: std::filesystem::path path_; EventHandler handler_; void *opaque_; }; /* ------------------------------------------------------------------------ */ /* Filesystem watcher. * This is the portable version for non-Windows (of which the Windows * specific version hasn't been written yet... TODO) */ /* We can't use std::recursive_directory_iterator! Why? Well, to put it blunt, it sucks. The main reason being is that if it finds a single directory it cannot recurse into, instead of safely recovering, it just completely stops. I am dumbfounded at this behavior, but nevertheless it means we simply can't use it, and must resort to old-fashioned recursion. --paper */ static void IterateDirectory(const std::filesystem::path &path, bool recursive, std::function<void(const std::filesystem::path &path)> func) { std::error_code ec; static const std::filesystem::directory_iterator end; for (std::filesystem::directory_iterator item(path, ec); item != end; item.increment(ec)) { if (ec) continue; std::filesystem::path p = item->path(); /* Hand the path off to the listener */ func(p); /* If we're dealing with another directory, recurse into it. */ if (recursive && item->is_directory()) IterateDirectory(p, true, func); } } class StdFilesystemWatcher : public Watcher { public: StdFilesystemWatcher(void *opaque, const std::filesystem::path &path, EventHandler handler, bool recursive) : Watcher(opaque, path, handler), recursive_(recursive) { } virtual ~StdFilesystemWatcher() override {} virtual void Process() override { /* Untoggle all paths. This allows us to only ever * iterate over the directory ONCE. */ UntoggleAllPaths(); /* Start iterating directories. If we're recursive, this * will go through the whole tree. */ IterateDirectory(path_, recursive_, [this](const std::filesystem::path &p) { if (FindAndTogglePath(p)) return; handler_(opaque_, p, Event::Created); paths_.insert({p, true}); }); DeleteUntoggledPaths(); } protected: bool FindAndTogglePath(const std::filesystem::path &p) { auto it = paths_.find(p); if (it == paths_.end()) return false; it->second = true; return true; } void UntoggleAllPaths() { for (auto &pp : paths_) pp.second = false; } void DeleteUntoggledPaths() { auto it = paths_.begin(); while (it != paths_.end()) { if (!it->second) { handler_(opaque_, it->first, Event::Deleted); it = paths_.erase(it); } else { it++; } } } /* unordered hashmap, path[found] */ std::unordered_map<std::filesystem::path, bool> paths_; bool recursive_; }; #ifdef WIN32 /* On Windows, we can ask the OS whether the folder has changed or not. * This is great for us! */ class Win32Watcher : public StdFilesystemWatcher { public: Win32Watcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler, bool recursive) : StdFilesystemWatcher(opaque, path, handler, recursive), dirwatcher_(INVALID_HANDLE_VALUE), first_(true) { } virtual ~Win32Watcher() override { // delete handle if (dirwatcher_ != INVALID_HANDLE_VALUE) FindCloseChangeNotification(dirwatcher_); } virtual void Process() override { if (first_ || dirwatcher_ == INVALID_HANDLE_VALUE) { /* We want to create this right before iteration so * we minimize possible race conditions while also * reducing unnecessary processing */ TryCreateDirWatcher(); StdFilesystemWatcher::Process(); first_ = false; return; } /* We have a valid handle */ if (WaitForSingleObject(dirwatcher_, 0) != WAIT_OBJECT_0) return; StdFilesystemWatcher::Process(); FindNextChangeNotification(dirwatcher_); } protected: bool TryCreateDirWatcher() { dirwatcher_ = FindFirstChangeNotificationW(Watcher::path_.wstring().c_str(), recursive_, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME); return (dirwatcher_ != INVALID_HANDLE_VALUE); } /* variables */ HANDLE dirwatcher_; bool first_; }; class Win32WatcherVista : public Win32Watcher { public: Win32WatcherVista(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler, bool recursive) : Win32Watcher(opaque, path, handler, recursive), dirhandle_(INVALID_HANDLE_VALUE) { overlapped_.hEvent = nullptr; } virtual ~Win32WatcherVista() override { if (dirhandle_ != INVALID_HANDLE_VALUE) CloseHandle(dirhandle_); if (overlapped_.hEvent != nullptr) CloseHandle(overlapped_.hEvent); } virtual void Process() override { if (first_) { if (TryCreateHandles()) { /* Queue a directory read asynchronously. On the next run, this will * be waited on. */ QueueDirectoryRead(); /* Avoid running Win32Watcher::Process, as it will read the whole * directory tree into memory. Instead, iterate through the directory * ourselves. */ IterateDirectory(path_, recursive_, [this](const std::filesystem::path &p) { handler_(opaque_, p, Event::Created); }); } else { /* Uh oh; we might have to fall back to Win32Watcher. Call into it to * load the tree into memory. */ Win32Watcher::Process(); } first_ = false; return; } if (!HandlesAreValid()) { /* If we're here, we already fell back into Win32Watcher, so just * call back into again. On the slim chance TryCreateHandles() might * succeed, call it as well... */ if (TryCreateHandles()) QueueDirectoryRead(); Win32Watcher::Process(); return; } if (WaitForSingleObject(overlapped_.hEvent, 0) != WAIT_OBJECT_0) return; FILE_NOTIFY_INFORMATION *ev = reinterpret_cast<FILE_NOTIFY_INFORMATION *>(change_buf_); for (;;) { /* Tack on the file name to the end of our path */ std::filesystem::path p = Watcher::path_ / std::wstring(ev->FileName, ev->FileNameLength / sizeof(WCHAR)); switch (ev->Action) { case FILE_ACTION_ADDED: case FILE_ACTION_RENAMED_NEW_NAME: /* File was added */ handler_(opaque_, p, Event::Created); break; case FILE_ACTION_REMOVED: case FILE_ACTION_RENAMED_OLD_NAME: /* File was removed */ handler_(opaque_, p, Event::Deleted); break; } if (ev->NextEntryOffset) { /* ugh ugh ugh ugh */ ev = reinterpret_cast<FILE_NOTIFY_INFORMATION *>(reinterpret_cast<char *>(ev) + ev->NextEntryOffset); } else { break; } } /* Queue a directory read for the next call */ QueueDirectoryRead(); } protected: bool HandlesAreValid() { return (overlapped_.hEvent && dirhandle_ != INVALID_HANDLE_VALUE); } bool TryCreateHandles() { if (!overlapped_.hEvent) { overlapped_.hEvent = CreateEventW(nullptr, FALSE, 0, nullptr); if (!overlapped_.hEvent) return false; } if (dirhandle_ == INVALID_HANDLE_VALUE) { dirhandle_ = CreateFileW(Watcher::path_.wstring().c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr); if (dirhandle_ == INVALID_HANDLE_VALUE) return false; } /* We're done here */ return true; } bool QueueDirectoryRead() { return ReadDirectoryChangesW(dirhandle_, change_buf_, sizeof(change_buf_), TRUE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME, nullptr, &overlapped_, nullptr); } HANDLE dirhandle_; OVERLAPPED overlapped_; alignas(FILE_NOTIFY_INFORMATION) char change_buf_[4096]; }; using DefaultWatcher = Win32WatcherVista; #else using DefaultWatcher = StdFilesystemWatcher; #endif IWatcher *GetRecursiveFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler) { /* .... :) */ return new DefaultWatcher(opaque, path, handler, true); } IWatcher *GetFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler) { return new DefaultWatcher(opaque, path, handler, false); } } // namespace Filesystem
