330 lines
10 KiB
Python
Executable File
330 lines
10 KiB
Python
Executable File
#! /usr/bin/env python
|
|
"""
|
|
Lha Virtual filesystem executive for Midnight Commander.
|
|
|
|
Tested against python 2.7, lha[1] 1.14 and mc 4.8.7
|
|
|
|
[1] http://lha.sourceforge.jp
|
|
|
|
Changelog:
|
|
1.0 Initial release
|
|
|
|
Author: Roman 'gryf' Dobosz <gryf73@gmail.com>
|
|
Date: 2013-05-05
|
|
Version: 1.0
|
|
Licence: BSD
|
|
"""
|
|
import os
|
|
import sys
|
|
import re
|
|
import shutil
|
|
from subprocess import call, check_call, check_output, CalledProcessError
|
|
from tempfile import mkdtemp, mkstemp
|
|
|
|
|
|
# Define which archiver you are using with appropriate options
|
|
ARCHIVER = "lha"
|
|
CMDS = {"list": "lq",
|
|
"read": "pq",
|
|
"write": "aq",
|
|
"delete": "dq"}
|
|
|
|
LINE_LHD = re.compile("^(?P<perms>[d-][rswx-]{9})"
|
|
"\s+(?P<uid>\d+)/"
|
|
"(?P<gid>\d+)"
|
|
"\s+(?P<size>\d+)"
|
|
"\s+(\*{6}|\d+\.\d%)"
|
|
"\s(?P<month>[JFMASOND][a-z]{2})\s+" # month
|
|
"(?P<day>\d+)\s+" # day
|
|
"(?P<yh>\d{4}|(\d{2}:\d{2}))" # year/hour
|
|
"\s(?P<fpath>.*)")
|
|
|
|
LINE_LHx = re.compile("^(?P<perms>(\[generic\])|(\[unknown\])|([d-][rswx-]{9}))"
|
|
"\s+(?P<size>\d+)"
|
|
"\s+(\*{6}|\d+\.\d%)"
|
|
"\s(?P<month>[JFMASOND][a-z]{2})\s+" # month
|
|
"(?P<day>\d+)\s+" # day
|
|
"(?P<yh>\d{4}|(\d{2}:\d{2}))" # year/hour
|
|
"\s(?P<fpath>.*)")
|
|
|
|
|
|
class Archive(object):
|
|
"""Archive handle. Provides interface to MC's extfs subsystem"""
|
|
def __init__(self, fname):
|
|
"""Prepare archive content for operations"""
|
|
self._filemap = {}
|
|
self._arch = fname
|
|
self._pattern = None
|
|
self._uid = str(os.getuid())
|
|
self._gid = str(os.getgid())
|
|
|
|
self._contents = self._get_dir()
|
|
|
|
def _identify(self):
|
|
"""Check for lha header"""
|
|
pat_map = {"-lhd-": LINE_LHD,
|
|
"-lh0-": LINE_LHx,
|
|
"-lh1-": LINE_LHx,
|
|
"-lh5-": LINE_LHx,
|
|
"-lh6-": LINE_LHx}
|
|
fobj = open(self._arch)
|
|
fobj.seek(2)
|
|
ident = fobj.read(5)
|
|
fobj.close()
|
|
return pat_map[ident]
|
|
|
|
def _map_name(self, name):
|
|
"""MC still have a bug in extfs subsystem, in case of filepaths with
|
|
leading space. This is workaround to this bug, which replaces leading
|
|
space with tilda. Real name is remembered in _filemap attribute and
|
|
used in real operations."""
|
|
if name.startswith(" "):
|
|
new_name = "".join(["~", name[1:]])
|
|
self._filemap[new_name] = name
|
|
return new_name
|
|
return name
|
|
|
|
def _get_real_name(self, name):
|
|
"""Get real filepath of the file. See _map_name docstring for
|
|
details."""
|
|
new_name = self._filemap.get(name)
|
|
if new_name:
|
|
return new_name
|
|
return name
|
|
|
|
def _get_dir(self):
|
|
"""Prepare archive file listing"""
|
|
if not self._pattern:
|
|
self._pattern = self._identify()
|
|
|
|
self._filemap = {}
|
|
contents = []
|
|
|
|
if self._pattern == LINE_LHx:
|
|
perms = "-rw-r--r--"
|
|
|
|
out = self._call_command("list")
|
|
if not out:
|
|
return
|
|
|
|
for line in out.split("\n"):
|
|
if line.endswith("/"):
|
|
line = line[:-1]
|
|
if self._pattern == LINE_LHx:
|
|
perms = "drw-r--r--"
|
|
|
|
match = self._pattern.match(line)
|
|
if not match:
|
|
continue
|
|
|
|
entry = match.groupdict()
|
|
# 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'] = self._uid
|
|
entry['gid'] = self._gid
|
|
|
|
if self._pattern == LINE_LHx:
|
|
entry['perms'] = perms
|
|
|
|
entry['display_name'] = self._map_name(entry['fpath'])
|
|
contents.append(entry)
|
|
return contents
|
|
|
|
def _call_command(self, cmd, src=None, dst=None):
|
|
"""
|
|
Return status of the provided command, which can be one of:
|
|
write
|
|
read
|
|
delete
|
|
list
|
|
"""
|
|
command = [ARCHIVER, CMDS.get(cmd), self._arch]
|
|
|
|
if src and dst:
|
|
command.append(src)
|
|
command.append(dst)
|
|
elif src or dst:
|
|
command.append(src and src or dst)
|
|
|
|
try:
|
|
output = check_output(command)
|
|
except CalledProcessError:
|
|
return None
|
|
return output
|
|
|
|
|
|
def list(self):
|
|
"""Output contents of the archive to stdout"""
|
|
for entry in self._contents:
|
|
sys.stdout.write("%(perms)s 1 %(uid)-8s %(gid)-8s %(size)8s "
|
|
"%(month)s %(day)s %(yh)s %(display_name)s\n" %
|
|
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 run(self, dst):
|
|
"""Execute file out of archive"""
|
|
fdesc, tmp_file = mkstemp()
|
|
os.close(fdesc)
|
|
result = 0
|
|
|
|
if self.copyout(dst, tmp_file) != 0:
|
|
result = 1
|
|
|
|
os.chmod(tmp_file, int("700", 8))
|
|
|
|
try:
|
|
result = 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 = mkdtemp()
|
|
arch_abspath = os.path.realpath(self._arch)
|
|
os.chdir(tmpdir)
|
|
if src:
|
|
os.makedirs(os.path.dirname(dst))
|
|
os.link(src, dst)
|
|
else:
|
|
os.makedirs(dst)
|
|
|
|
try:
|
|
result = check_call([ARCHIVER, CMDS["write"], arch_abspath, dst])
|
|
except CalledProcessError:
|
|
return 1
|
|
finally:
|
|
os.chdir(current_dir)
|
|
shutil.rmtree(tmpdir)
|
|
return result
|
|
|
|
def copyout(self, src, dst):
|
|
"""Copy file out form archive."""
|
|
src = self._get_real_name(src)
|
|
fobj = open(dst, "wb")
|
|
try:
|
|
result = check_call([ARCHIVER, CMDS['read'], self._arch, src],
|
|
stdout=fobj)
|
|
except CalledProcessError:
|
|
return 1
|
|
finally:
|
|
fobj.close()
|
|
return result
|
|
|
|
CALL_MAP = {'list': lambda a: Archive(a.arch).list(),
|
|
'copyin': lambda a: Archive(a.arch).copyin(a.src, a.dst),
|
|
'copyout': lambda a: Archive(a.arch).copyout(a.src, a.dst),
|
|
'mkdir': lambda a: Archive(a.arch).mkdir(a.dst),
|
|
'rm': lambda a: Archive(a.arch).rm(a.dst),
|
|
'run': lambda a: Archive(a.arch).run(a.dst)}
|
|
|
|
|
|
def parse_args():
|
|
"""Use ArgumentParser to check for script arguments and execute."""
|
|
parser = ArgumentParser()
|
|
subparsers = parser.add_subparsers(help='supported commands')
|
|
parser_list = subparsers.add_parser('list', help="List contents of "
|
|
"archive")
|
|
parser_copyin = subparsers.add_parser('copyin', help="Copy file into "
|
|
"archive")
|
|
parser_copyout = subparsers.add_parser('copyout', help="Copy file out of "
|
|
"archive")
|
|
parser_rm = subparsers.add_parser('rm', help="Delete file in archive")
|
|
parser_mkdir = subparsers.add_parser('mkdir', help="Create directory in "
|
|
"archive")
|
|
parser_run = subparsers.add_parser('run', help="Execute archived file")
|
|
|
|
parser_list.add_argument('arch', help="archive filename")
|
|
parser_list.set_defaults(func=CALL_MAP['list'])
|
|
|
|
parser_copyin.add_argument('arch', help="archive filename")
|
|
parser_copyin.add_argument('src', help="source filename")
|
|
parser_copyin.add_argument('dst', help="destination filename (to be "
|
|
"written into archive)")
|
|
parser_copyin.set_defaults(func=CALL_MAP['copyin'])
|
|
|
|
parser_copyout.add_argument('arch', help="archive filename")
|
|
parser_copyout.add_argument('src', help="source filename (to be read from"
|
|
" archive")
|
|
parser_copyout.add_argument('dst', help="destination filename")
|
|
parser_copyout.set_defaults(func=CALL_MAP['copyout'])
|
|
|
|
parser_rm.add_argument('arch', help="archive filename")
|
|
parser_rm.add_argument('dst', help="File inside archive to be deleted")
|
|
parser_rm.set_defaults(func=CALL_MAP['rm'])
|
|
|
|
parser_mkdir.add_argument('arch', help="archive filename")
|
|
parser_mkdir.add_argument('dst', help="Directory name inside archive to "
|
|
"be created")
|
|
parser_mkdir.set_defaults(func=CALL_MAP['mkdir'])
|
|
|
|
parser_run.add_argument('arch', help="archive filename")
|
|
parser_run.add_argument('dst', help="File to be executed")
|
|
parser_run.set_defaults(func=CALL_MAP['run'])
|
|
|
|
args = parser.parse_args()
|
|
return args.func(args)
|
|
|
|
|
|
def no_parse():
|
|
"""Failsafe argument "parsing". Note, that it blindly takes positional
|
|
arguments without checking them. In case of wrong arguments it will
|
|
silently exit"""
|
|
try:
|
|
if sys.argv[1] not in ('list', 'copyin', 'copyout', 'rm', 'mkdir',
|
|
"run"):
|
|
sys.exit(2)
|
|
except IndexError:
|
|
sys.exit(2)
|
|
|
|
class Arg(object):
|
|
"""Mimic argparse object"""
|
|
dst = None
|
|
src = None
|
|
arch = None
|
|
|
|
arg = Arg()
|
|
|
|
try:
|
|
arg.arch = sys.argv[2]
|
|
if sys.argv[1] in ('copyin', 'copyout'):
|
|
arg.src = sys.argv[3]
|
|
arg.dst = sys.argv[4]
|
|
elif sys.argv[1] in ('rm', 'run', 'mkdir'):
|
|
arg.dst = sys.argv[3]
|
|
except IndexError:
|
|
sys.exit(2)
|
|
|
|
return CALL_MAP[sys.argv[1]](arg)
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
from argparse import ArgumentParser
|
|
PARSE_FUNC = parse_args
|
|
except ImportError:
|
|
PARSE_FUNC = no_parse
|
|
|
|
sys.exit(PARSE_FUNC())
|