#!/usr/bin/env python3 # # MMGen Wallet, a terminal-based cryptocurrency wallet # Copyright (C)2013-2025 The MMGen Project # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ filename: File and MMGenFile classes and methods for the MMGen suite """ import sys, os from .util import die, get_extension class File: def __init__(self, fn, *, write=False): self.name = fn self.dirname = os.path.dirname(fn) self.basename = os.path.basename(fn) self.ext = get_extension(fn) self.mtime = None self.ctime = None self.atime = None try: st = os.stat(fn) except: die('FileNotFound', f'{fn!r}: file not found') import stat if stat.S_ISBLK(st.st_mode): if sys.platform in ('win32',): die(2, 'Access to raw block devices not supported on platform {sys.platform!r}') mode = (os.O_RDONLY, os.O_RDWR)[bool(write)] try: fd = os.open(fn, mode) except OSError as e: if e.errno == 13: die(2, f'{fn!r}: permission denied') # if e.errno != 17: raise else: if sys.platform == 'linux': self.size = os.lseek(fd, 0, os.SEEK_END) elif sys.platform == 'darwin': from .platform.darwin.util import get_device_size self.size = get_device_size(fn) os.close(fd) else: self.size = st.st_size self.mtime = st.st_mtime self.ctime = st.st_ctime self.atime = st.st_atime class FileList(list): def __init__(self, fns, *, write=False): list.__init__( self, [File(fn, write=write) for fn in fns]) def names(self): return [f.name for f in self] def sort_by_age(self, *, key='mtime', reverse=False): assert key in ('atime', 'ctime', 'mtime'), f'{key!r}: invalid sort key' self.sort(key=lambda a: getattr(a, key), reverse=reverse) class MMGenFile(File): def __init__(self, fn, *, base_class=None, subclass=None, proto=None, write=False): """ 'base_class' - a base class with an 'ext_to_cls' method 'subclass' - a subclass with an 'ext' attribute One or the other must be provided, but not both. The base class signals support for the MMGenFile API by setting its 'filename_api' attribute to True. """ super().__init__(fn, write=write) assert (subclass or base_class) and not (subclass and base_class), 'MMGenFile chk1' if not getattr(subclass or base_class, 'filename_api', False): die(3, f'Class {(subclass or base_class).__name__!r} does not support the MMGenFile API') if base_class: subclass = base_class.ext_to_cls(self.ext, proto) if not subclass: die('BadFileExtension', f'{self.ext!r}: not a recognized file extension for {base_class}') self.subclass = subclass class MMGenFileList(FileList): def __init__(self, fns, base_class, *, proto=None, write=False): list.__init__( self, [MMGenFile(fn, base_class=base_class, proto=proto, write=write) for fn in fns]) def find_files_in_dir(subclass, fdir, *, no_dups=False): assert isinstance(subclass, type), f'{subclass}: not a class' if not getattr(subclass, 'filename_api', False): die(3, f'Class {subclass.__name__!r} does not support the MMGenFile API') matches = [l for l in os.listdir(fdir) if l.endswith('.'+subclass.ext)] if no_dups: if len(matches) == 1: return os.path.join(fdir, matches[0]) elif matches: die(1, f'ERROR: more than one {subclass.__name__} file in directory {fdir!r}') else: return None else: return [os.path.join(fdir, m) for m in matches] def find_file_in_dir(subclass, fdir): return find_files_in_dir(subclass, fdir, no_dups=True)