summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorB. Stack <bgstack15@gmail.com>2024-11-19 16:25:24 -0500
committerB. Stack <bgstack15@gmail.com>2024-11-19 16:25:24 -0500
commitee9d031d650f5af3e4063ea82c3debafca0ccca3 (patch)
treeb6a584fb577e92de028facbe1553137cbec1edaf
downloadgmm-ee9d031d650f5af3e4063ea82c3debafca0ccca3.tar.gz
gmm-ee9d031d650f5af3e4063ea82c3debafca0ccca3.tar.bz2
gmm-ee9d031d650f5af3e4063ea82c3debafca0ccca3.zip
initial commit
-rwxr-xr-xgmm303
1 files changed, 303 insertions, 0 deletions
diff --git a/gmm b/gmm
new file mode 100755
index 0000000..00f040e
--- /dev/null
+++ b/gmm
@@ -0,0 +1,303 @@
+#!/usr/bin/env python3
+# vim: set et ts=4 sts=4 sw=4:
+# Startdate: 2024-11-18-2 13:58
+# Title: Graphical Mount Manager
+# Purpose: Easily mount iso files and easily manage these mounted files and mount points, basically like acetoneiso
+# Dependencies:
+# req-devuan: python3, python3-psutil
+# References:
+# https://stackoverflow.com/questions/23662280/how-to-log-the-contents-of-a-configparser/50362738#50362738
+
+import argparse, sys, os, psutil, subprocess, json, configparser
+
+# LOW-LEVEL values
+appversion = "0.0.1"
+conffile = os.path.join(os.getenv("HOME"),".config","gmm","config")
+
+# LOW-LEVEL FUNCTIONS
+def ferror(*args, **kwargs):
+ print(*args, file=sys.stderr, **kwargs)
+
+def debuglev(_numbertocheck):
+ # if _numbertocheck <= debuglevel then return truthy
+ _debuglev = False
+ try:
+ if int(_numbertocheck) <= int(debuglevel):
+ _debuglev = True
+ except Exception as e:
+ pass
+ return _debuglev
+
+# APP FUNCTIONS
+def list_mounts():
+ """
+ Return a python list of FILE,MOUNT pairs
+ """
+ # get list of devices
+ mounts = [
+ {
+ "device": i.device,
+ "mountpoint": i.mountpoint
+ } for i in psutil.disk_partitions(False) if i.fstype in ["iso9660"]
+ ]
+ # use losetup --json,
+ losetup_p = subprocess.Popen(
+ ["/usr/sbin/losetup","--json"], stdout=subprocess.PIPE
+ )
+ losetup_o, _ = losetup_p.communicate()
+ try:
+ losetup_j = json.loads(losetup_o)
+ except Exception as e:
+ ferror(f"Got error {e} when trying to read losetup output")
+ losetup_j = {}
+ if losetup_j:
+ losetup_j = losetup_j["loopdevices"]
+ for i in mounts:
+ #for j in losetup_j["loopdevices"]:
+ # if i.device == j.name:
+ try:
+ i["source"] = [j["back-file"] for j in losetup_j if j["name"] == i["device"]][0]
+ except Exception as e:
+ ferror(f"While looking for {i['device']} in {losetup_j}, got error {e}")
+ for i in mounts:
+ if "source" in i:
+ i.pop("device")
+ # Now we have a list of
+ # [{'mountpoint': '/mnt/foo', 'source': '/mnt/public/CDROMs/Games/Logical Journey of the Zoombinis.iso'}]
+ return mounts
+
+def mount_iso_to_path(isofile, mount_point):
+ """
+ Mount the isofile to mount_point using sudo. No protections of already-mounted or something-else-mounted.
+ """
+ mount_p = subprocess.Popen(
+ ["sudo","mount","-v",isofile,mount_point], stdout=subprocess.PIPE
+ )
+ mount_o, mount_e = mount_p.communicate()
+ if debuglev(5):
+ if mount_e:
+ ferror(mount_e)
+
+def mount_iso_to_default(isofile, config = None):
+ """
+ Use the gmm config to determine where to mount this.
+ """
+ if not config:
+ raise Exception(f"Fatal: Cannot determine where to mount. Check config file, probably {conffile}.")
+ md = config["gmm"]["mounts_dir"]
+ # determine next available number. 0-indexed. If you want to make this 1-indexed, set x to 0.
+ x = -1
+ safe = False
+ # safety valve: do not try mount more than 20 mount points.
+ while (not safe) or x < 20:
+ x = x + 1
+ testpath = os.path.join(md,str(x))
+ if not os.path.exists(testpath):
+ safe = True
+ break
+ else:
+ if os.path.ismount(testpath):
+ if iso_is_mounted_to_path(isofile, testpath):
+ if debuglev(2):
+ ferror(f"INFO: Found {isofile} mounted at {testpath} already. Skipping...")
+ # short-circuit
+ return True
+ # if a mountpoint but is not the requested isofile
+ if debuglev(8):
+ ferror(f"DEBUG: Found different mountpoint at {testpath}, continuing to look...")
+ # skip this number
+ continue
+ elif os.path.isdir(testpath):
+ if os.listdir(testpath):
+ # not empty so skip it
+ ferror(f"WARNING: non-mountpoint {testpath} is not empty, continuing to look...")
+ continue
+ else:
+ # empty directory, this is what we want
+ safe = True
+ break
+ else:
+ # so not a mountpoint and not a dir, so skip
+ ferror(f"WARNING: non-directory {testpath}, continuing to look...")
+ continue
+ # end while
+ # notably, because of all the continues and breaks, we cannot increment x at bottom of this loop.
+ if debuglev(8):
+ ferror(f"DEBUG: after loop, testpath is {testpath}")
+ if safe:
+ if debuglev(1):
+ print(f"INFO: will mount {isofile} to {testpath}.")
+ try:
+ os.mkdir(testpath)
+ # FUTUREIMPROVEMENT: might need to catch specific exceptions and pass them, like dir-already-exists.
+ except:
+ pass
+ mount_iso_to_path(isofile, testpath)
+
+def unmount_iso_to_path(isofile, mount_point):
+ """
+ Unmount the isofile to mount_point using sudo.
+ This exploits that the "isofile" could be the actual iso file that is mounted, or the mount point. The umount command can either to unmount something.
+ """
+ params = ["sudo","umount","-v",isofile,mount_point]
+ params = [i for i in params if i]
+ if debuglev(1):
+ ferror(f"Running: {params}")
+ mount_p = subprocess.Popen(
+ params, stdout=subprocess.PIPE
+ )
+ mount_o, mount_e = mount_p.communicate()
+ if debuglev(5):
+ if mount_e:
+ ferror(mount_e)
+
+def get_iso_mounted_to_path(mount_point, mounts = None):
+ """
+ Return the filename of what is mounted to mount_point.
+ """
+ if not os.path.ismount(mount_point):
+ return False
+ if not mounts:
+ mounts = list_mounts()
+ for i in mounts:
+ if i["mountpoint"] == mount_point:
+ # short-circuit because there can be only one thing mounted to a mount_point
+ return i["source"]
+ return None
+
+def iso_is_mounted_to_path(iso_file, mount_point, mounts = None):
+ """
+ Return True if the named iso_file is mounted to mount_point.
+ """
+ if not os.path.ismount(mount_point):
+ return False
+ if not mounts:
+ mounts = list_mounts()
+ for i in mounts:
+ if i["mountpoint"] == mount_point and i["source"] == iso_file:
+ # short-circuit
+ return True
+ return False
+
+# GRAPICAL FUNCTIONS
+
+# GRAPHICAL APP
+# want mount iso... file dialog, that has two mimetype choices: *.iso, or *
+# a combo box/list box thing of mounted isos, mount point
+# a config file/dialog for choosing default mounts dir, which is managed by this app. E.g., /mnt/iso so the first disc is /mnt/iso/1 or something. Or ~/mnt/iso. Something like that.
+
+# PARSE ARGUMENTS
+parser = argparse.ArgumentParser(description="graphical mount manager",prog="gmm",epilog=""" The value-add of this application is to use a configured directory for
+mountpoints, for easy mounting, e.g., from a graphical file manager. Run with an
+iso filename, and it will mount to ~/mnt/0, for example.""")
+parser.add_argument("-d","--debug", nargs='?', default=0, type=int, choices=range(0,11), help="Set debug level.")
+parser.add_argument("-V","--version", action="version", version="%(prog)s " + appversion)
+parser.add_argument("-g","--gui", action=argparse.BooleanOptionalAction, default=None, help="Show gui, even if also given any other actions.")
+parser.add_argument("-l","--list", action="store_true", help="List mounted isos and paths and then exit.")
+parser.add_argument("-o","--output", default="tsv", choices=("tsv","raw","json"), help="Change output format for --list.")
+parser.add_argument("-u","--unmount","--umount", action="store_true", help="If used with one or more paths, umount them instead of mounting.")
+parser.add_argument("-a","--all", action="store_true", help="Used only with --unmount. Unmount all mounted paths in default-configured dir.")
+parser.add_argument("paths", nargs="*",help="Positional parameters: ISO_FILE, MOUNT_PATH")
+#parser.add_argument("-f","--force", action="store_true", help="Force deploy all confs.")
+parser.add_argument("-c","--conf","--config", action="store", default=conffile, help="Use this config file.")
+#parser.add_argument("-r","--dry","--dryrun","--dry-run", action="store_true", help="Do not execute. Useful when debugging.")
+args = parser.parse_args()
+if args.debug is None:
+ debuglevel = 5
+elif args.debug:
+ debuglevel = args.debug
+if debuglev(1):
+ ferror("debug level", debuglevel)
+ ferror(args)
+paths = args.paths
+umount = args.unmount
+show_gui = True
+
+# Parse config
+config = configparser.ConfigParser()
+if args.conf:
+ conffile = args.conf
+config.read(conffile)
+try:
+ #config["gmm"]["mounts_dir"] = os.path.expanduser(config["gmm"]["mounts_dir"])
+ config.set("gmm","mounts_dir", os.path.expanduser(config["gmm"]["mounts_dir"].strip('"')))
+except:
+ pass
+if debuglev(9):
+ ferror({section: dict(config[section]) for section in config.sections()})
+
+# MAIN
+if "__main__" == __name__:
+ mounts = list_mounts()
+ # default behavior is to show the gui, unless --no-gui, or given some paths to mount, or --list
+ if (args.gui == False) or (args.list) or (paths):
+ show_gui = False
+ if args.gui == True:
+ show_gui = True
+ if args.list:
+ if args.output == "json":
+ print(json.dumps(mounts))
+ elif args.output == "tsv":
+ if mounts:
+ print("source\tpath")
+ for i in mounts:
+ print(f"{i['source']}\t{i['mountpoint']}")
+ else:
+ print(mounts)
+ if paths:
+ # calculate if paths.len = 2, then check if paths[0] is a file and paths[1] is a dir or underneath
+ if len(paths) == 2:
+ left = paths[0]
+ right = paths[1]
+ if os.path.isfile(left):
+ if os.path.isdir(right) or (not os.paths.exists(right)):
+ if os.path.ismount(right):
+ if debuglev(8):
+ ferror(f"DEBUG: Investigating current mount {right} to see if it is same as {left}")
+ if iso_is_mounted_to_path(left, right, mounts):
+ if umount:
+ unmount_iso_to_path(left, right)
+ else:
+ if debuglev(1):
+ print(f"Already mounted!")
+ else:
+ ferror(f"ERROR: {get_iso_mounted_to_path(right,mounts)} is already mounted to {right}.")
+ # FUTUREIMPROVEMENT: If we want to implement --force to umount old, then mount this left, do it here.
+ else:
+ if umount:
+ if debuglev(2):
+ print(f"INFO: not a mount point, so nothing to do.")
+ else:
+ if debuglev(1):
+ print(f"INFO: will mount {left} to {right}.")
+ mount_iso_to_path(left, right)
+ elif os.path.isfile(right):
+ for i in path:
+ mount_iso_to_default(i,config)
+ else:
+ ferror(f"ERROR: second path {right} is not a dir or file. Choose a different spot")
+ else:
+ ferror(f"ERROR: first path is not a file: {left}. Skipping...")
+ elif len(paths) == 1:
+ # main default method; the path to the iso to mount
+ isofile = paths[0]
+ # not ideal, but figure out that the user wanted "--list"
+ if isofile == "list" and not os.path.isfile(isofile):
+ ferror(f"WARNING: please use --list to list mounts. Continuing...")
+ print(mounts)
+ sys.exit(0)
+ if umount:
+ unmount_iso_to_path(isofile,"")
+ else:
+ mount_iso_to_default(isofile,config)
+ else:
+ # more than one path, so treat each as a file to mount
+ for isofile in paths:
+ if umount:
+ unmount_iso_to_path(isofile,"")
+ else:
+ mount_iso_to_default(isofile,config)
+ if show_gui:
+ # do tkinter app here
+ # WORKHERE: entire graphical app
+ print(f"STUB DEBUG: please run tkinter app now")
bgstack15