Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create mergerfs.findfile #144

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions src/mergerfs.findfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
import argparse
import ctypes
import errno
import glob
import os
import sys
import stat

_libc = ctypes.CDLL("libc.so.6", use_errno=True)
_lgetxattr = _libc.lgetxattr
_lgetxattr.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t]

def lgetxattr(path, name):
if type(path) == str:
path = path.encode(errors='backslashreplace')
if type(name) == str:
name = name.encode(errors='backslashreplace')
length = 64
while True:
buf = ctypes.create_string_buffer(length)
res = _lgetxattr(path, name, buf, ctypes.c_size_t(length))
if res >= 0:
return buf.raw[0:res]
else:
err = ctypes.get_errno()
if err == errno.ERANGE:
length *= 2
elif err == errno.ENODATA:
return None
else:
raise IOError(err, os.strerror(err), path)

def xattr_basepath(fullpath):
basepath = lgetxattr(fullpath, 'user.mergerfs.basepath')
if basepath is not None:
return basepath.decode(errors='backslashreplace')
return None

def xattr_relpath(fullpath):
relpath = lgetxattr(fullpath, 'user.mergerfs.relpath')
if relpath is not None:
return relpath.decode(errors='backslashreplace')
return None

def mergerfs_srcmounts(ctrlfile):
srcmounts_raw = lgetxattr(ctrlfile, 'user.mergerfs.srcmounts')
if srcmounts_raw is not None:
srcmounts = srcmounts_raw.decode(errors='backslashreplace').split(':')
expanded_srcmounts = []
for srcmount in srcmounts:
expanded_srcmounts.extend(glob.glob(srcmount))
return expanded_srcmounts
return []

def mergerfs_control_file(path):
while path != os.path.sep:
ctrlfile = os.path.join(path, '.mergerfs')
if os.path.exists(ctrlfile):
return ctrlfile
path = os.path.abspath(os.path.join(path, os.pardir))
return None

def parse_arguments():
parser = argparse.ArgumentParser(description='Find file location in mergerfs and handle duplicates with deletion option.')
parser.add_argument('file_path', type=str, help='File path to check')
parser.add_argument('-d', '--delete', action='store_true', help='Generate delete commands for duplicates')
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose mode to show searching process')
return parser.parse_args()

if __name__ == "__main__":
args = parse_arguments()

file_path = args.file_path
relpath = xattr_relpath(file_path)
ctrlfile = mergerfs_control_file(file_path)
if ctrlfile is None:
print("# Error: Could not find the .mergerfs control file.")
sys.exit(1)

srcmounts = mergerfs_srcmounts(ctrlfile)
basepath = xattr_basepath(file_path)
found_files = []

if basepath and relpath:
first_found = True
for srcmount in srcmounts:
potential_path = os.path.join(srcmount, relpath.lstrip('/'))
if args.verbose:
print(f"{'# ' if args.delete else ''}Searching {srcmount}")
if os.path.exists(potential_path):
found_files.append(potential_path)
escaped_path = potential_path.replace('"', '\\"')
if first_found:
print(f"{'# ' if args.delete else ''}Full physical path of the file: {escaped_path}")
first_found = False
else:
if args.delete:
print(f'rm -vf "{escaped_path}"')
elif not args.delete:
print(f"{'# ' if args.delete else ''}***Duplicate: {escaped_path}")
else:
if not args.delete:
print("Could not find the full physical path for the file or the file is not part of a mergerfs pool.")