view src/core/filesystem.cc @ 399:a0bc3ae5164a

anime_list: actually obey "highlight anime if they are available"
author Paper <paper@tflc.us>
date Fri, 07 Nov 2025 15:28:22 -0500
parents 650a9159a0e7
children 2f89797b6a44
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";
}

/* ------------------------------------------------------------------------ */
/* This is the "base class" for basically every watcher. */

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; however, pretty much
 * every implementation should be based on this in some shape or form. */

/*
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_;
};

/* There does actually exist an API in Qt to do file system watching.
 * However, it is of little use for us, since it
 *  a) isn't recursive
 *  b) requires constant conversion to and from QString, which takes
 *     up valuable system resources
 *  c) uses signals etc. that are a pain in the ass to use in an
 *     event-processing logic like we do here. */

#ifdef WIN32
/* On Windows, we can ask the OS whether the directory tree 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_;
};

/* I think "Vista" is actually a misnomer, MSDN says that ReadDirectoryChangesW
 * has existed since Windows XP.
 *
 * Anyway, this is a version that doesn't need to keep the whole directory
 * tree in-memory if it is available. */
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