commit 9c872636d649a676ee3d1335fd5a18a917e20f8a Author: Fierelier Date: Tue Oct 10 14:39:33 2023 +0200 Initial commit 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 0000000..0fee7d6 Binary files /dev/null and b/module/py/me/fier/python/__pycache__/__init__.cpython-311.pyc differ diff --git a/module/tools/7z.py b/module/tools/7z.py new file mode 100644 index 0000000..dbb17a6 --- /dev/null +++ b/module/tools/7z.py @@ -0,0 +1,102 @@ +import os +api = mfp.require("api") +prochelper = mfp.require("prochelper") +filehelper = mfp.require("filehelper") + +def split7zLine(line): + arg = 0 + args = [] + argchars = "" + for c in line: + if arg == 5: + argchars = argchars + c + continue + if c in [" ","\t"]: + if argchars == "": continue + if arg == 0: # Date + if argchars.count("-") != 2: # Not a date, assume time is missing as well. Add both. + args.append("0000-00-00") + args.append("00:00:00") + arg += 2 + + args.append(argchars) + + if arg == 2: # Attr + if argchars[0] == "D": # Is directory, insert 0 (compressed) size + args.append("0") + args.append("0") + arg += 2 + + argchars = "" + arg += 1 + else: + argchars = argchars + c + + if argchars != "": + args.append(argchars) + + return args + +def action_list(archiveFile,flags): + status = 0 + for line in prochelper.pcallLines(["7z","l",archiveFile]): + if status == 0: + if line.lstrip(" \t").startswith("Date"): + status = 1 + continue + + if status == 1: + status = 2 + continue + + if status == 2: + if line.startswith("-"): return + lineSplit = split7zLine(line) + + apiFile = api.getDefaultFile() + apiFile["isdir"] = lineSplit[2].startswith("D") + apiFile["date"] = list(map(int,lineSplit[0].split("-"))) + apiFile["time"] = list(map(int,lineSplit[1].split(":"))) + apiFile["size"] = int(lineSplit[3]) + apiFile["sizeCompressed"] = int(lineSplit[4]) + apiFile["name"] = lineSplit[5] + + apiFile["ownerGroup"] = -1 + apiFile["ownerUser"] = -1 + apiFile["permissions"] = "---------" + + yield apiFile + +def action_extract(archiveFile,flags): + import shutil + if flags["if"] == "": + prochelper.pcall(["7z","x",archiveFile,"-y","-aoa","-o" + flags["of"]]) + return + + try: + tempdir = mfp.p(flags["of"],"7z." +str(os.getpid())+ ".tmp") + outdir = mfp.p(tempdir,mfp.pUp(flags["if"].replace("\\",os.path.sep).replace("/",os.path.sep))) + os.mkdir(tempdir) + prochelper.pcall(["7z","x",archiveFile,"-y","-aoa","-o" + tempdir,flags["if"]]) + + if os.path.isdir(outdir): + for root,dirs,files in os.walk(outdir): + for file in dirs: + ffile = mfp.p(root,file) + nfile = mfp.p(flags["of"],ffile.replace(outdir + os.path.sep,"",1)) + if not os.path.isdir(nfile): os.mkdir(nfile) + + for file in files: + ffile = mfp.p(root,file) + nfile = mfp.p(flags["of"],ffile.replace(outdir + os.path.sep,"",1)) + if os.path.isfile(nfile): os.remove(nfile) + os.rename(ffile,nfile) + else: + nfile = mfp.p(flags["of"],outdir.replace(mfp.pUp(outdir) + os.path.sep,"",1)) + if os.path.isfile(nfile): os.remove(nfile) + os.rename(outdir,nfile) + + filehelper.rmtree(tempdir) + except: + filehelper.rmtree(tempdir) + raise diff --git a/module/tools/tar.py b/module/tools/tar.py new file mode 100644 index 0000000..d8c7bc7 --- /dev/null +++ b/module/tools/tar.py @@ -0,0 +1,82 @@ +import os +api = mfp.require("api") +prochelper = mfp.require("prochelper") +filehelper = mfp.require("filehelper") + +def splitTarLine(line): + args = [] + argchars = "" + arg = 0 + for c in line: + if arg == 5: + argchars += c + continue + + if c in [" ","\t"]: + if argchars != "": + args.append(argchars) + argchars = "" + continue + argchars = argchars + c + + if argchars != "": + args.append(argchars) + + return args + +def action_list(archiveFile,flags): + status = 0 + for line in prochelper.pcallLines(["tar","--numeric-owner","-tvf",archiveFile]): + if line.strip(" \t\r") == "": continue + lineSplit = splitTarLine(line) + + apiFile = api.getDefaultFile() + apiFile["isdir"] = (lineSplit[0][0] == "d") + apiFile["date"] = list(map(int,lineSplit[3].split("-"))) + apiFile["time"] = list(map(int,lineSplit[4].split(":"))) + [0] + apiFile["size"] = int(lineSplit[2]) + apiFile["sizeCompressed"] = int(lineSplit[2]) + if not apiFile["isdir"]: + apiFile["name"] = lineSplit[5] + else: + apiFile["name"] = lineSplit[5][:-1] + + apiFile["ownerGroup"] = lineSplit[1].split("/")[0] + apiFile["ownerUser"] = lineSplit[1].split("/")[1] + apiFile["permissions"] = lineSplit[0][1:] + + yield apiFile + +def action_extract(archiveFile,flags): + import shutil + if flags["if"] == "": + prochelper.pcall(["tar","-xvf",archiveFile,"--overwrite","-C",flags["of"]]) + return + + try: + tempdir = mfp.p(flags["of"],"tar." +str(os.getpid())+ ".tmp") + outdir = mfp.p(tempdir,mfp.pUp(flags["if"].replace("\\",os.path.sep).replace("/",os.path.sep))) + os.mkdir(tempdir) + prochelper.pcall(["tar","-xvf",archiveFile,"--overwrite","-C",tempdir,flags["if"]]) + + if os.path.isdir(outdir): + for root,dirs,files in os.walk(outdir): + for file in dirs: + ffile = mfp.p(root,file) + nfile = mfp.p(flags["of"],ffile.replace(outdir + os.path.sep,"",1)) + if not os.path.isdir(nfile): os.mkdir(nfile) + + for file in files: + ffile = mfp.p(root,file) + nfile = mfp.p(flags["of"],ffile.replace(outdir + os.path.sep,"",1)) + if os.path.isfile(nfile): os.remove(nfile) + os.rename(ffile,nfile) + else: + nfile = mfp.p(flags["of"],outdir.replace(mfp.pUp(outdir) + os.path.sep,"",1)) + if os.path.isfile(nfile): os.remove(nfile) + os.rename(outdir,nfile) + + filehelper.rmtree(tempdir) + except: + filehelper.rmtree(tempdir) + raise diff --git a/uninstall b/uninstall new file mode 100755 index 0000000..ae0ec6c --- /dev/null +++ b/uninstall @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -e +MY_DIR="$(dirname "$(realpath "$BASH_SOURCE")")" +export UNINSTALL=1 +exec "$MY_DIR/install"