Initial commit

This commit is contained in:
Fierelier 2023-10-10 14:39:33 +02:00
commit 9c872636d6
16 changed files with 611 additions and 0 deletions

19
LICENSE Normal file
View File

@ -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.

35
README.txt Normal file
View File

@ -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 <action> <flags> <archive>
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.

28
app Executable file
View File

@ -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()

1
appname.txt Normal file
View File

@ -0,0 +1 @@
spitzip

3
config.txt Normal file
View File

@ -0,0 +1,3 @@
APP_NAME="$(cat $MY_DIR/appname.txt)"
APP_DIR="/opt/$APP_NAME"
APP_BIN_DIR="/usr/local/bin"

29
install Executable file
View File

@ -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"

16
module/api.py Normal file
View File

@ -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)

13
module/filehelper.py Normal file
View File

@ -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")

124
module/main.py Normal file
View File

@ -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]+ '''" <action> <flags> <archive>
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)

3
module/printhelper.py Normal file
View File

@ -0,0 +1,3 @@
import sys
def eprint(*args, **kwargs):
print(*args,file=sys.stderr,**kwargs)

69
module/prochelper.py Normal file
View File

@ -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

View File

@ -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"))

102
module/tools/7z.py Normal file
View File

@ -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

82
module/tools/tar.py Normal file
View File

@ -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

5
uninstall Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
MY_DIR="$(dirname "$(realpath "$BASH_SOURCE")")"
export UNINSTALL=1
exec "$MY_DIR/install"