mirror of https://github.com/trapexit/mergerfs.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
444 lines
9.6 KiB
444 lines
9.6 KiB
#include "mergerfs_fsck.hpp"
|
|
|
|
#include "fs_lchmod.hpp"
|
|
#include "fs_lchown.hpp"
|
|
#include "fs_close.hpp"
|
|
#include "fs_lgetxattr.hpp"
|
|
#include "fs_lstat.hpp"
|
|
#include "fs_open.hpp"
|
|
#include "int_types.h"
|
|
#include "str.hpp"
|
|
#include "fs_copyfile.hpp"
|
|
#include "mergerfs_api.hpp"
|
|
|
|
#include "fmt/core.h"
|
|
#include "fmt/chrono.h"
|
|
#include "CLI11.hpp"
|
|
|
|
#include <filesystem>
|
|
|
|
namespace FS = std::filesystem;
|
|
|
|
struct PathStat
|
|
{
|
|
PathStat(const std::string &path_)
|
|
: path(path_),
|
|
st{}
|
|
{
|
|
}
|
|
|
|
std::string path;
|
|
struct stat st;
|
|
};
|
|
|
|
using PathStatVec = std::vector<PathStat>;
|
|
using FixFunc = std::function<void(const PathStatVec&,const bool)>;
|
|
|
|
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));
|
|
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,
|
|
*std::localtime(&st.st_mtime));
|
|
}
|
|
|
|
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<std::string> 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;
|
|
}
|