From 9c872636d649a676ee3d1335fd5a18a917e20f8a Mon Sep 17 00:00:00 2001 From: Fierelier Date: Tue, 10 Oct 2023 14:39:33 +0200 Subject: [PATCH] Initial commit --- LICENSE | 19 +++ README.txt | 35 +++++ app | 28 ++++ appname.txt | 1 + config.txt | 3 + install | 29 ++++ module/api.py | 16 +++ module/filehelper.py | 13 ++ module/main.py | 124 ++++++++++++++++++ module/printhelper.py | 3 + module/prochelper.py | 69 ++++++++++ module/py/me/fier/python/__init__.py | 82 ++++++++++++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 3966 bytes module/tools/7z.py | 102 ++++++++++++++ module/tools/tar.py | 82 ++++++++++++ uninstall | 5 + 16 files changed, 611 insertions(+) create mode 100644 LICENSE create mode 100644 README.txt create mode 100755 app create mode 100644 appname.txt create mode 100644 config.txt create mode 100755 install create mode 100644 module/api.py create mode 100644 module/filehelper.py create mode 100644 module/main.py create mode 100644 module/printhelper.py create mode 100644 module/prochelper.py create mode 100644 module/py/me/fier/python/__init__.py create mode 100644 module/py/me/fier/python/__pycache__/__init__.cpython-311.pyc create mode 100644 module/tools/7z.py create mode 100644 module/tools/tar.py create mode 100755 uninstall diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ebf116b --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..d25118e --- /dev/null +++ b/README.txt @@ -0,0 +1,35 @@ +A script that can list/extract files from archives, by automatically choosing the correct tool and translating the tool's input/output into the same format, when it comes to listing files. + +This isn't really meant to be used by people. It's supposed to act as an API for other programs that interact with archives. + +--- + +Syntax: spitzip + +Available actions: + * help - Prints this help + * list - Outputs each file of archive as a json table + * extract - Copy a file/folder from the archive into another folder. -if= sets +the path to copy from the archive (DO NOT use wildcards!), -of= sets the output +folder. The output folder has to exist. Files are replaced without asking. + +Universal flags: + * -tool= - Which tool to use for the archive. By default, the program guesses +the best tool for the job. Available: 7z, tar + +--- + +Prerequisites: + * Python 3 (Version 3.4 or up) + * 7-zip (on Linux, p7zip) + * tar + * Optional: file (for detecting mimetype) + +Installation: + * sudo ./install + * Note: Installation is not required. You can run the program portably by using ./app + +Uninstallation: + * sudo /opt/spitzip/uninstall + +You can change the name of the program using appname.txt, and change other settings like installation directory and bin directory from config.txt. diff --git a/app b/app new file mode 100755 index 0000000..1d5bee6 --- /dev/null +++ b/app @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +def init(): + app = mfp.require("main") + +def main(): + mfp.setProgramName(open(mfp.p(mfp.sd,"appname.txt"),"r",encoding="utf-8").read().strip(" \t\r\n")) + app = mfp.require("main") + app.main() + +def bootstrap(name,modName): + if name in globals(): return + import sys, os, importlib + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + s = os.path.realpath(sys.executable) + else: + s = os.path.realpath(__file__) + + if not os.path.join(os.path.dirname(s),"module","py") in sys.path: + sys.path = [os.path.join(os.path.dirname(s),"module","py")] + sys.path + + mod = importlib.import_module(modName); modl = mod.Bunch() + mod.init(mod,modl,s,name) + globals()[name] = mod; globals()[name + "l"] = modl + +bootstrap("mfp","me.fier.python") +init() +if __name__ == '__main__': + main() diff --git a/appname.txt b/appname.txt new file mode 100644 index 0000000..a5af4d4 --- /dev/null +++ b/appname.txt @@ -0,0 +1 @@ +spitzip diff --git a/config.txt b/config.txt new file mode 100644 index 0000000..3f57014 --- /dev/null +++ b/config.txt @@ -0,0 +1,3 @@ +APP_NAME="$(cat $MY_DIR/appname.txt)" +APP_DIR="/opt/$APP_NAME" +APP_BIN_DIR="/usr/local/bin" diff --git a/install b/install new file mode 100755 index 0000000..e360594 --- /dev/null +++ b/install @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -e +MY_DIR="$(dirname "$(realpath "$BASH_SOURCE")")" +source "$MY_DIR/config.txt" + +if ! [ "$UNINSTALL" == "1" ]; then + if [ "$APP_DIR" == "$MY_DIR" ]; then + echo "This copy is already installed." + exit 1 + fi +fi + +if [ -d "$APP_DIR" ]; then + rm -r "$APP_DIR" +fi + +if [ -L "$APP_BIN_DIR/$APP_NAME" ]; then + rm "$APP_BIN_DIR/$APP_NAME" +fi + +if [ "$UNINSTALL" == "1" ]; then + exit 0 +fi + +mkdir -p "$APP_DIR" +cp -r "$MY_DIR/." "$APP_DIR" +mkdir -p "$APP_BIN_DIR" +ln -s "$APP_DIR/app" "$APP_BIN_DIR/$APP_NAME" +chmod +x "$APP_BIN_DIR/$APP_NAME" diff --git a/module/api.py b/module/api.py new file mode 100644 index 0000000..f8e47a4 --- /dev/null +++ b/module/api.py @@ -0,0 +1,16 @@ +import copy +_file = { + "isdir": False, + "date": [0,0,0], + "time": [0,0,0], + "size": 0, + "sizeCompressed": 0, + "name": "", + + "ownerGroup": -1, + "ownerUser": -1, + "permissions": "---------" +} + +def getDefaultFile(): + return copy.deepcopy(_file) diff --git a/module/filehelper.py b/module/filehelper.py new file mode 100644 index 0000000..6c8e6e3 --- /dev/null +++ b/module/filehelper.py @@ -0,0 +1,13 @@ +import os +import shutil +import time + +def rmtree(path): + shutil.rmtree(path) + if not os.path.isdir(path): return + waited = 0 + while waited < 100: + if not os.path.isdir(path): return + time.sleep(0.1) + waited += 1 + raise Exception("Directory survived shutil.rmtree") diff --git a/module/main.py b/module/main.py new file mode 100644 index 0000000..f05666d --- /dev/null +++ b/module/main.py @@ -0,0 +1,124 @@ +import sys +import os +prochelper = mfp.require("prochelper") +prochelper.setLanguage("C") +eprint = mfp.require("printhelper").eprint + +def getFlag(flags,flag): + if not flag in flags: return None + return flags[flag] + +def getArchiveTool(archiveFile): + tool = "7z" + + while True: + # Method 1: file command + try: + mimetype = prochelper.pcallStr(["file","-b","--mime-type","--uncompress",archiveFile]) + if mimetype == "application/x-tar": + tool = "tar" + break + except subprocess.CalledProcessError as e: + eprint("Warning: 'file' exited with error " +str(e.returncode)) + + # Method 2: file extension + archiveFileSplit = archiveFile.rsplit(".",2) + if len(archiveFileSplit) < 2: + eprint("Warning: No file extension.") + break + + if archiveFileSplit[-1].lower() == "tar": + tool = "tar" + break + + if len(archiveFileSplit) > 2 and archiveFileSplit[-2].lower() == "tar": + tool = "tar" + break + + break + + return tool + +def printHelp(exitCode): + eprint('''\ +Syntax: "''' +sys.argv[0]+ '''" + +Available actions: + * help - Prints this help + * list - Outputs each file of archive as a json table + * extract - Copy a file/folder from the archive into another folder. -if= sets +the path to copy from the archive (DO NOT use wildcards!), -of= sets the output +folder. The output folder has to exist. Files are replaced without asking. + +Universal flags: + * -tool= - Which tool to use for the archive. By default, the program guesses +the best tool for the job. Available: 7z, tar\ +''') + sys.exit(exitCode) + +def main(): + args = sys.argv[1:] + if len(args) > 0: + action = args.pop(0) + else: + eprint("ERROR: No action given.\n") + printHelp(1) + + flags = {} + index = 0 + length = len(args) + while index < length: + arg = args[index] + if arg.startswith("-") and "=" in arg: + arg = arg[1:].split("=",1) + flags[arg[0]] = arg[1] + del args[index] + length -= 1 + continue + index += 1 + + if length > 1: + eprint("ERROR: Too many, or malformed arguments.\n") + printHelp(1) + + if length < 1: + eprint("ERROR: No archive file in arguments.\n") + printHelp(1) + + archiveFile = args[0] + if not os.path.isfile(archiveFile): + eprint("ERROR: " +archiveFile+ " not found.") + printHelp(1) + + if action == "help": printHelp(0) + + tool = getFlag(flags,"tool") + if tool == None: + tool = getArchiveTool(archiveFile) + toolApi = mfp.require("tools/" +tool) + + if action == "list": + import json + for apiFile in toolApi.action_list(archiveFile,flags): + print(json.dumps(apiFile)) + return + + if action == "extract": + if not "if" in flags: + eprint("ERROR: -if flag is not set.\n") + printHelp(1) + + if not "of" in flags: + eprint("ERROR: -of flag is not set.\n") + printHelp(1) + + if not os.path.isdir(flags["of"]): + eprint("ERROR: -of '" + str(flags["of"]) + "' is not a directory.") + sys.exit(1) + + toolApi.action_extract(archiveFile,flags) + return + + eprint("ERROR: Invalid action.\n") + printHelp() + sys.exit(1) diff --git a/module/printhelper.py b/module/printhelper.py new file mode 100644 index 0000000..8106573 --- /dev/null +++ b/module/printhelper.py @@ -0,0 +1,3 @@ +import sys +def eprint(*args, **kwargs): + print(*args,file=sys.stderr,**kwargs) diff --git a/module/prochelper.py b/module/prochelper.py new file mode 100644 index 0000000..341bf0c --- /dev/null +++ b/module/prochelper.py @@ -0,0 +1,69 @@ +import subprocess +import os +def perr(rtn,cmd = None,op = None): + if rtn == 0: return + if cmd == None: cmd = [] + exc = subprocess.CalledProcessError(rtn,cmd,op) + raise exc + +def pcall(*args,**kwargs): + rtn = subprocess.Popen(*args,**kwargs).wait() + perr(rtn,args[0]) + +def pcallStr(*args,**kwargs): + proc = subprocess.Popen(*args,**kwargs, stdout=subprocess.PIPE) + response = proc.stdout.read().decode("utf-8").strip("\n") + rtn = proc.wait() + perr(rtn,args[0]) + return response + +def pcallLines(*args,**kwargs): + proc = subprocess.Popen(*args,**kwargs, stdout=subprocess.PIPE) + line = b"" + while True: + b = proc.stdout.read(1) + if b == b"": + yield line.decode("utf-8") + break + if b == b"\n": + yield line.decode("utf-8") + line = b"" + continue + line = line + b + rtn = proc.wait() + perr(rtn,args[0]) + +langStore = False +def setLanguage(lang = None): + def getLanguageEnvs(): + envs = [] + for env in os.environ: + if env.startswith("LC_"): + envs.append(env) + continue + + if env in ["LANGUAGE","LANG"]: + envs.append(env) + continue + + return envs + + def unsetLanguage(): + for env in getLanguageEnvs(): + del os.environ[env] + + if lang == None: + if langStore == False: return + unsetLanguage() + for env in langStore: os.environ[env] = langStore[env] + langStore = False + return + + langStore = {} + for env in getLanguageEnvs(): + langStore[env] = os.environ[env] + + unsetLanguage() + os.environ["LANGUAGE"] = lang + os.environ["LC_ALL"] = lang + os.environ["LANG"] = lang diff --git a/module/py/me/fier/python/__init__.py b/module/py/me/fier/python/__init__.py new file mode 100644 index 0000000..af0b6e6 --- /dev/null +++ b/module/py/me/fier/python/__init__.py @@ -0,0 +1,82 @@ +import os + +# BUNCH +try: + import munch + Bunch = munch.Munch + bunchify = munch.munchify + unbunchify = munch.unmunchify +except Exception: + import bunch + Bunch = munch.Bunch + bunchify = munch.bunchify + unbunchify = munch.unbunchify + +# GLOBALS +g = Bunch() + +# SCRIPTS +paths = [] +loaded = Bunch() + +def docode(st,name = "Unknown"): + code = compile(st,name,"exec") + glb = Bunch() + glb[distro] = me + glb[distro + "l"] = Bunch() + glb[distro + "l"].s = name + try: + glb[distro + "l"].sd = pUp(name) + except Exception: + pass + glb[distro + "l"].g = glb + exec(code,glb) + return glb + +def dofile(path): + return docode(open(path,"rb").read(),path) + +def dorequire(name,*args,**kwargs): + name = name.replace("/",".").replace("\\",".") + for path in paths: + path = path.replace("?",name.replace(".",os.path.sep)) + if os.path.isfile(path): + return dofile(path,*args,**kwargs) + raise Exception("Library " +name+ " not found.") + +def require(name,*args,**kwargs): + if not name in loaded: + loaded[name] = dorequire(name,*args,**kwargs) + return loaded[name] + +# PROGRAM +programName = None +programNameSet = False +def setProgramName(name): + global programName,programNameSet + if programNameSet: return + programNameSet = True + programName = name + +# INIT +inited = False +def init(mod,modl,sp,d): + global inited + if inited: return + global distro,me,p,pUp,programName,programNameSet,s,sd,distro + import sys + inited = True + distro = d + me = mod + + p = os.path.join + pUp = os.path.dirname + s = sp + sd = pUp(sp) + modl.s = s + modl.sd = sd + programName = distro + "." + s.replace(modl.sd + os.path.sep,"",1).rsplit(".",1)[0] + programNameSet = False + + paths.append(p(modl.sd,"module","?.py")) + paths.append(p(modl.sd,"module","?","_main.py")) diff --git a/module/py/me/fier/python/__pycache__/__init__.cpython-311.pyc b/module/py/me/fier/python/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fee7d6e02220d3b41081331acaa418035bab260 GIT binary patch literal 3966 zcmbtXO>7&-6`t8$?hi?kBB`I~C$wTojtLv49H(`FCUxpKG8(i(6Du`BxE5BNl|+Xk znb}oj88o~y=!OoIh6PeY4O)aK3e~M$pobLbvB@b%(pbR60t8$XKBOlC1`0u^zFCS( zu7BV|hvc{O_h#R`dGDLYKlSzb5tM)ZcT&3}BJ^+8=oDeU^6)=U`5X~Ma2BPI(yrkY z*6<~+GdaaWt0RF@K4`&u?UIsmQ-ZD}Q?zzp7gc3@Hhz~GKM8JN6)d-Q)D3PHRgW3R5 zpf*VGIg|)|$JU;+rFV1h=kn{hw7W^@W=b-(9~Lk(iUHUuMO4HD@4)D7cVkyM!>uFa zRs1S%xCTS0$Q!P~5PB6BeY&J~F5#}gY$j2}PZ}BND2Nu^_bJE-xZNPP1N*q`?&TS) zxO(RLCy^2At^)_mW7K&@O9f8)p-K zTT16w3z@8D`%bUr(knK%s@WpR=myQ(ShxAYy9JxmiLJbOIjt3pOg?AhWy(%4A;5rP z3po`;gmj*0Hou%*)R}!IpzNJZuH;v>q)LsIHJUt=Pp_?NIYWnz^h)MKZ9y$$lB;>L zmerDljU>!m%4ig-#!5byTv*8DGRDHfvBC!ElV{7*L0EYRe)?YkOw^Ra6|VB~wpf=B zSn>gLpeY46*GubWtQn4&u^T_%9BU4a{`&l9=UJni-n>x9(Z=vtr)A-&g$G(3&q>XZ z@$LEFj{I)KoV>AA);F^j7qhsb8aK4tlp`&KIjIW`PBf=lJoN4E?%p3B^{&^%K9iJB zY{pA*Gv1H`=BOS8b-#`uJdM#e*nc|2f76En)2^$)FmnedBo?9K4T)YCE&_je54GwZ zF9^ED8ze>Kl>sN<=4_5GCV0oDwvaDqIa{EbN)p%>3aYW9Gwmkm03fsf61WH*gT@s6 z^g{s5{-IBd%9-tfo7c9k-3w3N4Num?(^h!8O6u|vOFmM=M;^jVK0XP02>{SS$r$`G=eLvSrEM1oE^0QYvZ_7(d zUFoXJ&%K+0Km}WLse?w1zV3LDU&clJ2mB7oKv4e?{|cu$jLxC0RfE}azYt#$F$yCh zL+^UmaYCBg!^u5-Y!6@9!>6vuKgldom2OO22~6bj#>7&7Ek}+eWLu(IA*-f=4jd=2 zxx7wCVL_YMwSp~Xbk`9m9TH%@2|6`BjD0N+l3U8r`iW=8LH6)2)qbC z{civ!YRJJ)gC7Sg=eA>Y`FTrz{+>L2R|X%tb)qgOEjbBs-4|^ko;&1#Ip0wFJ{|vf zyprCY`ts-(N2~f@U%GSh&JXINC#}(wb>$UHdBvP+`lQnNk1k%nSUFSojaa@Bh+qCt z)8AhisQITW20;6}y|iEDr)$m^BAY+-KZ20>Cj?I4~mP6%GGcvmUaxIdq4gj>jx5W1I#-Z}S z=23|8vauDc;Q?pov!1Opif~A40xCdSfSp1)0xk+E0&GFWtPYej588(cG`~#M)j7B| z&S{1n=xHagvzuP(3F(^gv#tc4fft;FpU#}pL=S{$4Ubl4E3=L05YTNnW{$B>`#}Sm71Bku-#FNnsj~aOsz1k9q$X-pn_QUA^T3aFh9q^Oe zuCF}0y5qZnIm+&od3pfQM&B{dpS8ty_Hsoy1z8Gx0s5}U3#)1-2hBtb*b*`rjR19S z=#-r*RTxY$VD4)Rm+~1%rwX=2GL)rJl${-A2SwRwP-Zkb4Pc8@FJvG6 zp&2VQQ^&_G%(%M!b*KQCg#~|N$g#fdTXk|>-%r_tvT(3vrz^9xVatoGm(~1hIky+; zoqp%$vwcpJGU|8Ef*xTtmZCa&l0(mw@ovwVG4`V0WAHk_pW&|@tF!RmlV5E+(+}y7 zp~?J9?*oL(2;&A)Oy|=Q6?~w9`^<6nX(*wsF>|mHjBL%B&ox3ZD->skx7(2VTS&kM zy6{XHbF`&^1SGVr5c2uWH#g6g&YHL-A|+t@;7#Sho5Nefrq~jKIM_m77{~Z1+^-!F zMf_3=xzNw!LAbg*5JbSzfktG=47E4`5^zv;A413