#!/usr/bin/env python3 """ Lha Virtual filesystem executive for Midnight Commander. Tested against python 3.8, lha[1] 1.14 and mc 4.8.29 [1] http://lha.sourceforge.jp Changelog: 1.4 Cleanup code, use latin1 1.3 Switch to python3 1.2 Moved item pattern to extfslib module 1.1 Moved common code into extfslib library 1.0 Initial release Author: Roman 'gryf' Dobosz Date: 2023-10-22 Version: 1.4 Licence: BSD """ import os import re import shutil import subprocess import sys import tempfile import extfslib class ULha(extfslib.Archive): """Archive handle. Provides interface to MC's extfs subsystem""" LINE_PAT = re.compile(r"^((?P[d-][rswx-]{9})|(\[generic\])|" r"(\[unknown\]))" r"((\s+\d+/\d+\s+)|(\s+))" r"(?P)(?P)" # just for the record r"(?P\d+)" r"\s+(\*{6}|\d+\.\d%)" r"\s(?P[JFMASOND][a-z]{2})\s+" # month r"(?P\d+)\s+" # day r"(?P\d{4}|(\d{2}:\d{2}))" # year/hour r"\s(?P.*)") ARCHIVER = "lha" CMDS = {"list": "lq", "read": "pq", "write": "aq", "delete": "dq"} DATETIME = "%(month)s %(day)s %(yh)s" def _get_dir(self): """Prepare archive file listing""" contents = [] out = subprocess.run([self.ARCHIVER, self.CMDS['list'], self._arch], capture_output=True, encoding="Latin1") if out.returncode != 0: sys.stderr.write(out.stderr) return for line in out.stdout.split("\n"): # -lhd- can store empty directories perms = "-rw-r--r--" if line.endswith(os.path.sep): line = line[:-1] perms = "drw-r--r--" match = self.LINE_PAT.match(line) if not match: continue match_entry = match.groupdict() entry = {} for key in match_entry: entry[key] = match_entry[key] del match_entry # UID and GID sometimes can have strange values depending on # the information that was written into archive. Most of the # times I was dealing with Amiga lha archives, so that i don't # really care about real user/group entry['uid'] = str(self._uid) entry['gid'] = str(self._gid) entry['datetime'] = self.DATETIME % entry if not entry['perms']: entry['perms'] = perms entry['display_name'] = self._map_name(entry['fpath']) contents.append(entry) return contents def list(self): """Output contents of the archive to stdout""" for entry in self._contents: sys.stdout.write(self.ITEM.decode('utf-8') % entry) return 0 def rm(self, dst): """Remove file from archive""" dst = self._get_real_name(dst) # deleting with quiet option enabled will output nothing, so we get # empty string here or None in case of error. Not smart. if self._call_command('delete', dst=dst) is None: return 1 return 0 def rmdir(self, dst): """Remove empty directory""" dst = self._get_real_name(dst) if not dst.endswith(os.path.sep): dst += os.path.sep res = subprocess.run([self.ARCHIVER, self.CMDS['delete'], dst], capture_output=True) return res.returncode def run(self, dst): """Execute file out of archive""" fdesc, tmp_file = tempfile.mkstemp() os.close(fdesc) result = 0 if self.copyout(dst, tmp_file) != 0: result = 1 os.chmod(tmp_file, int("700", 8)) try: result = subprocess.call([tmp_file]) finally: try: os.unlink(tmp_file) except OSError: pass return result def mkdir(self, dst): """Create empty directory in archive""" return self.copyin(dst) def copyin(self, dst, src=None): """Copy file to the archive or create direcotry inside. If src is empty, create empty directory with dst name.""" current_dir = os.path.abspath(os.curdir) tmpdir = tempfile.mkdtemp() arch_abspath = os.path.realpath(self._arch) os.chdir(tmpdir) if src: os.makedirs(os.path.dirname(dst)) shutil.copy2(src, dst) else: os.makedirs(dst) res = subprocess.run([self.ARCHIVER, self.CMDS["write"], arch_abspath, dst], capture_output=True, encoding='utf-8') os.chdir(current_dir) shutil.rmtree(tmpdir) return res.returncode def copyout(self, src, dst): """Copy file out form archive.""" src = self._get_real_name(src) with open(dst, "wb") as fobj: res = subprocess.run([self.ARCHIVER, self.CMDS['read'], self._arch, src], stdout=fobj) return res.returncode if __name__ == "__main__": sys.exit(extfslib.parse_args(ULha))