Files
mc_ulha/ulha

176 lines
5.3 KiB
Python
Executable File

#!/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 <gryf73@gmail.com>
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<perms>[d-][rswx-]{9})|(\[generic\])|"
r"(\[unknown\]))"
r"((\s+\d+/\d+\s+)|(\s+))"
r"(?P<uid>)(?P<gid>)" # just for the record
r"(?P<size>\d+)"
r"\s+(\*{6}|\d+\.\d%)"
r"\s(?P<month>[JFMASOND][a-z]{2})\s+" # month
r"(?P<day>\d+)\s+" # day
r"(?P<yh>\d{4}|(\d{2}:\d{2}))" # year/hour
r"\s(?P<fpath>.*)")
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))