/* ISC License Copyright (c) 2026, Antonio SJ Musumeci Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include "mergerfs_fsck.hpp" #include "fs_close.hpp" #include "fs_copyfile.hpp" #include "fs_lchmod.hpp" #include "fs_lchown.hpp" #include "fs_lgetxattr.hpp" #include "fs_lstat.hpp" #include "fs_open.hpp" #include "mergerfs_api.hpp" #include "str.hpp" #include "fmt/core.h" #include "fmt/chrono.h" #include "CLI11/CLI11.hpp" #include "base_types.h" #include namespace FS = std::filesystem; struct PathStat { PathStat(const std::string &path_) : path(path_), st{} { } std::string path; struct stat st; }; using PathStatVec = std::vector; using FixFunc = std::function; static bool _files_differ(const PathStatVec &paths_, const bool check_size_) { if(paths_.size() <= 1) return false; const struct stat &st0 = paths_[0].st; for(u64 i = 1; i < paths_.size(); i++) { const struct stat &st = paths_[i].st; if(st.st_mode != st0.st_mode) return true; if(st.st_uid != st0.st_uid) return true; if(st.st_gid != st0.st_gid) return true; if(check_size_ && S_ISREG(st0.st_mode) && (st.st_size != st0.st_size)) return true; } return false; } static bool _files_same_type(const PathStatVec &paths_) { mode_t type; if(paths_.size() <= 1) return true; type = (paths_[0].st.st_mode & S_IFMT); for(u64 i = 1; i < paths_.size(); i++) { if((paths_[i].st.st_mode & S_IFMT) != type) return false; } return true; } static const char* _file_type(const mode_t mode_) { switch(mode_ & S_IFMT) { case S_IFBLK: return "block dev"; case S_IFCHR: return "char dev"; case S_IFDIR: return "directory"; case S_IFIFO: return "fifo"; case S_IFLNK: return "symlink"; case S_IFSOCK: return "socket"; case S_IFREG: return "regular"; } return "unknown"; } static void _compare_files(const std::string &mergerfs_path_, PathStatVec &pathstats_, const FixFunc fix_func_, const bool check_size_, const bool copy_file_) { int rv; if(pathstats_.size() <= 1) return; for(auto &pathstat : pathstats_) { rv = fs::lstat(pathstat.path,&pathstat.st); if(rv < 0) pathstat.st.st_size = rv; } if(!::_files_differ(pathstats_,check_size_)) return; fmt::println("* {}",mergerfs_path_); for(u64 i = 0; i < pathstats_.size(); i++) { const auto &path = pathstats_[i].path; const auto &st = pathstats_[i].st; fmt::println(" {}: {}",i,path); if(st.st_size < 0) fmt::println(" - err: {}",strerror(-st.st_size)); time_t mtime = st.st_mtime; std::tm *mtime_tm = std::localtime(&mtime); fmt::println(" -" " uid: {};" " gid: {};" " mode: {:05o};" " type: {};" " size: {};" " mtime: {:%Y-%m-%dT%H:%M:%S%z}", st.st_uid, st.st_gid, st.st_mode, ::_file_type(st.st_mode), st.st_size, *mtime_tm); } if(!fix_func_) return; if(::_files_same_type(pathstats_)) fix_func_(pathstats_,copy_file_); else fmt::println(" X: WARNING - files are of different types." " Requires manual intervention."); } static int _get_allpaths(const std::string &mergerfs_path_, PathStatVec &pathstats_) { int rv; std::vector allpaths; rv = mergerfs::api::allpaths(mergerfs_path_,allpaths); if(rv < 0) return rv; for(const auto &path : allpaths) pathstats_.emplace_back(path); return 0; } static u64 _read_u64(const u64 min_, const u64 max_) { u64 rv; while(true) { fmt::print("Select correct file: "); std::cin >> rv; if((rv >= min_) && (rv <= max_)) return rv; std::cin.clear(); std::cin.ignore(100, '\n'); } } static void _setattr(const struct stat &st_, const PathStatVec &pathstats_) { int rv; for(const auto &path : pathstats_) { rv = fs::lchmod(path.path,st_.st_mode); rv |= fs::lchown(path.path,st_.st_uid,st_.st_gid); if(rv) fmt::println("ERROR: problem setting mode or owner - {}", strerror(errno)); } } static void _copy_files(const std::string &src_, const PathStatVec &dsts_) { for(const auto &dst : dsts_) { int rv; // TODO: Additional protection to confirm not the literal same // file. fs::is_same_file(src,dst); if(src_ == dst.path) continue; rv = fs::copyfile(src_,dst.path,{.cleanup_failure=false}); if(rv < 0) fmt::print(stderr, "ERROR: failed copying to {} - {}", dst.path, strerror(errno)); } } static bool _mtime_cmp(const struct stat &a_, const struct stat &b_) { if(a_.st_mtim.tv_sec == b_.st_mtim.tv_sec) return (a_.st_mtim.tv_nsec < b_.st_mtim.tv_nsec); return (a_.st_mtim.tv_sec < b_.st_mtim.tv_sec); } static bool _pathstat_cmp(const PathStatVec::value_type &a_, const PathStatVec::value_type &b_) { return ::_mtime_cmp(a_.st,b_.st); } static const PathStatVec::value_type* _find_latest_mtime(const PathStatVec &pathstats_) { PathStatVec::const_iterator i; i = std::max_element(pathstats_.begin(), pathstats_.end(), ::_pathstat_cmp); return &(*i); } static const PathStatVec::value_type* _find_largest(const PathStatVec &pathstats_) { PathStatVec::const_iterator i; i = std::max_element(pathstats_.begin(), pathstats_.end(), [](const PathStatVec::value_type &a_, const PathStatVec::value_type &b_) { return (a_.st.st_size < b_.st.st_size); }); return &(*i); } static void _fix_func_manual(const PathStatVec &pathstats_, const bool copy_file_) { u64 idx; idx = ::_read_u64(0ULL,pathstats_.size()-1); if(copy_file_ && S_ISREG(pathstats_[idx].st.st_mode)) ::_copy_files(pathstats_[idx].path,pathstats_); else ::_setattr(pathstats_[idx].st,pathstats_); } static void _fix_func_newest(const PathStatVec &pathstats_, const bool copy_file_) { const PathStatVec::value_type *pathst; pathst = _find_latest_mtime(pathstats_); if(!pathst) return; if(copy_file_ && S_ISREG(pathst->st.st_mode)) ::_copy_files(pathst->path,pathstats_); else ::_setattr(pathst->st,pathstats_); }; static void _fix_func_largest(const PathStatVec &pathstats_, const bool copy_file_) { const PathStatVec::value_type *pathst; pathst = ::_find_largest(pathstats_); if(!pathst) return; if(copy_file_ && S_ISREG(pathst->st.st_mode)) ::_copy_files(pathst->path,pathstats_); else ::_setattr(pathst->st,pathstats_); } static FixFunc _select_fix_func(const std::string &fix_) { if(fix_ == "none") return {}; else if(fix_ == "manual") return ::_fix_func_manual; else if(fix_ == "newest") return ::_fix_func_newest; else if(fix_ == "largest") return ::_fix_func_largest; else abort(); return {}; } static void _fsck(const FS::path &path_, const FixFunc fix_func_, const bool check_size_, const bool copy_file_) { int rv; PathStatVec paths; ::_get_allpaths(path_,paths); ::_compare_files(path_,paths,fix_func_,check_size_,copy_file_); auto opts = FS::directory_options::skip_permission_denied; auto rdi = FS::recursive_directory_iterator(path_,opts); for(const auto &de : rdi) { paths.clear(); rv = ::_get_allpaths(de.path(),paths); if(rv < 0) continue; ::_compare_files(de.path(), paths, fix_func_, check_size_, copy_file_); } } int mergerfs::fsck::main(int argc_, char **argv_) { CLI::App app; FS::path path; std::string fix; bool check_size; bool copy_file; FixFunc fix_func; app.description("fsck.mergerfs:" " A tool to help diagnose and solve mergerfs pool issues"); app.name("USAGE: fsck.mergerfs"); app.add_option("path",path) ->description("mergerfs path") ->check(CLI::ExistingDirectory) ->required(); app.add_option("--fix",fix) ->description("Will attempt to 'fix' the problem by chown+chmod or copying files based on a selected file.\n" " * none: Do nothing. Just print details.\n" " * manual: User selects source file.\n" " * newest: Use file with most recent mtime.\n" " * largest: Use file with largest size.") ->check(CLI::IsMember({"none","manual","newest","largest"})) ->default_val("none"); app.add_option("--check-size",check_size) ->description("Considers file size in calculating differences") ->default_val(false) ->default_str("false"); app.add_option("--copy-file",copy_file) ->description("Copy file rather than chown/chmod to fix") ->default_val(false) ->default_str("false"); try { app.parse(argc_,argv_); } catch(const CLI::ParseError &e) { return app.exit(e); } if((geteuid() != 0) && (fix != "none")) fmt::println(stderr,"WARNING: fsck.mergerfs should be run as root to apply fixes"); fix_func = ::_select_fix_func(fix); ::_fsck(path, fix_func, check_size, copy_file); return 0; }