Compare commits

...

2 Commits

Author SHA1 Message Date
b1c2a496c8 Cleanup code, use latin1 encoding for amiga file names. 2023-10-22 18:59:47 +02:00
0d529baa8b Normalize imports 2023-10-22 16:12:06 +02:00
2 changed files with 80 additions and 72 deletions

View File

@@ -14,19 +14,32 @@ on my Amiga. Both reading from and writing into archive was implemented.
Requirements Requirements
------------ ------------
ULha requires `free lha <http://lha.sourceforge.jp>`_ implementation to work. ULha requires free `lha`_ implementation to work. There is another lha
implementation under development - `lhasa`_, although it is currently limited
to list and extract only.
Limitations
-----------
For filenames which contain other characters outside of ASCII set, there is no
way to operate on them as `lha`_ simply ignore those files. Extracting full
archive will extract also those files, although names would be incorrectly
decoded, and hence, unusable on the AmigaOS. Looking forward for the `lhasa`_
to be completed, and then will switch to it.
Installation Installation
------------ ------------
* install `extfslib`_ * install `extfslib`_
* copy ``ulha`` to ``~/.local/share/mc/extfs.d/`` * copy ``ulha`` to ``~/.local/share/mc/extfs.d/``
* add or change entry for files handle in ``~/.config/mc/mc.ext``:: * add or change entry for files handle in ``~/.config/mc/mc.ext``:
# lha .. code::ini
regex/\.[lL]([Hh][aA]|[Zz][hH])$
[lha]
Type=^LHa\ .*archive
Open=%cd %p/ulha:// Open=%cd %p/ulha://
View=%view{ascii} lha l %f View=%view{ascii} /usr/libexec/mc/ext.d/archive.sh view lha
License License
======= =======
@@ -36,3 +49,5 @@ details.
.. _extfslib: https://github.com/gryf/mc_extfslib .. _extfslib: https://github.com/gryf/mc_extfslib
.. _lha: http://lha.sourceforge.jp
.. _lhasa: https://github.com/fragglet/lhasa

119
ulha
View File

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