2022-07-16 07:33:25 +00:00
|
|
|
# Python imports
|
2022-11-29 04:34:13 +00:00
|
|
|
import os
|
|
|
|
import threading
|
|
|
|
import subprocess
|
|
|
|
import time
|
|
|
|
import pwd
|
|
|
|
import grp
|
2022-07-16 07:33:25 +00:00
|
|
|
from datetime import datetime
|
|
|
|
|
2022-07-21 04:57:06 +00:00
|
|
|
# Lib imports
|
2022-07-16 07:33:25 +00:00
|
|
|
import gi
|
|
|
|
gi.require_version('Gtk', '3.0')
|
2022-11-29 04:34:13 +00:00
|
|
|
from gi.repository import Gtk
|
|
|
|
from gi.repository import GLib
|
|
|
|
from gi.repository import Gio
|
2022-07-16 07:33:25 +00:00
|
|
|
|
|
|
|
# Application imports
|
2022-09-06 02:21:04 +00:00
|
|
|
from plugins.plugin_base import PluginBase
|
2022-07-16 07:33:25 +00:00
|
|
|
|
|
|
|
|
|
|
|
# NOTE: Threads WILL NOT die with parent's destruction.
|
|
|
|
def threaded(fn):
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
# NOTE: Threads WILL die with parent's destruction.
|
|
|
|
def daemon_threaded(fn):
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Properties:
|
|
|
|
file_uri: str = None
|
|
|
|
file_name: str = None
|
|
|
|
file_location: str = None
|
|
|
|
file_target: str = None
|
|
|
|
mime_type: str = None
|
|
|
|
file_size: str = None
|
|
|
|
mtime: int = None
|
|
|
|
atime: int = None
|
|
|
|
file_owner: str = None
|
|
|
|
file_group: str = None
|
|
|
|
chmod_stat: str = None
|
|
|
|
|
|
|
|
|
2022-09-06 02:21:04 +00:00
|
|
|
class Plugin(PluginBase):
|
2022-07-16 07:33:25 +00:00
|
|
|
def __init__(self):
|
2022-09-06 02:21:04 +00:00
|
|
|
super().__init__()
|
|
|
|
|
2022-08-13 03:54:16 +00:00
|
|
|
self.path = os.path.dirname(os.path.realpath(__file__))
|
2022-09-06 02:21:04 +00:00
|
|
|
self._GLADE_FILE = f"{self.path}/file_properties.glade"
|
2022-08-13 03:54:16 +00:00
|
|
|
self.name = "Properties" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
|
|
|
# where self.name should not be needed for message comms
|
2022-07-16 07:33:25 +00:00
|
|
|
|
2022-09-06 02:21:04 +00:00
|
|
|
self._properties_dialog = None
|
2022-07-16 07:33:25 +00:00
|
|
|
self._file_name = None
|
|
|
|
self._file_location = None
|
|
|
|
self._file_target = None
|
|
|
|
self._mime_type = None
|
|
|
|
self._file_size = None
|
|
|
|
self._mtime = None
|
|
|
|
self._atime = None
|
|
|
|
self._file_owner = None
|
|
|
|
self._file_group = None
|
|
|
|
|
|
|
|
self._chmod_map: {} = {
|
|
|
|
"7": "rwx",
|
|
|
|
"6": "rw",
|
|
|
|
"5": "rx",
|
|
|
|
"4": "r",
|
|
|
|
"3": "wx",
|
|
|
|
"2": "w",
|
|
|
|
"1": "x",
|
|
|
|
"0": ""
|
|
|
|
}
|
|
|
|
|
|
|
|
self._chmod_map_counter: {} = {
|
|
|
|
"rwx": "7",
|
|
|
|
"rw": "6",
|
|
|
|
"rx": "5",
|
|
|
|
"r": "4",
|
|
|
|
"wx": "3",
|
|
|
|
"w": "2",
|
|
|
|
"x": "1",
|
|
|
|
"": "0"
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-09-06 02:21:04 +00:00
|
|
|
def run(self):
|
2022-07-16 07:33:25 +00:00
|
|
|
self._builder = Gtk.Builder()
|
|
|
|
self._builder.add_from_file(self._GLADE_FILE)
|
|
|
|
|
|
|
|
self._properties_dialog = self._builder.get_object("file_properties_dialog")
|
|
|
|
self._file_name = self._builder.get_object("file_name")
|
|
|
|
self._file_location = self._builder.get_object("file_location")
|
|
|
|
self._file_target = self._builder.get_object("file_target")
|
|
|
|
self._mime_type = self._builder.get_object("mime_type")
|
|
|
|
self._file_size = self._builder.get_object("file_size")
|
|
|
|
self._mtime = self._builder.get_object("mtime")
|
|
|
|
self._atime = self._builder.get_object("atime")
|
|
|
|
self._file_owner = self._builder.get_object("file_owner")
|
|
|
|
self._file_group = self._builder.get_object("file_group")
|
|
|
|
|
2022-10-10 01:59:44 +00:00
|
|
|
def generate_reference_ui_element(self):
|
2022-10-26 04:27:21 +00:00
|
|
|
item = Gtk.ImageMenuItem(self.name)
|
|
|
|
item.set_image( Gtk.Image(stock=Gtk.STOCK_PROPERTIES) )
|
|
|
|
item.connect("activate", self._show_properties_page)
|
|
|
|
item.set_always_show_image(True)
|
|
|
|
return item
|
2022-07-16 07:33:25 +00:00
|
|
|
|
|
|
|
|
|
|
|
@threaded
|
|
|
|
def _show_properties_page(self, widget=None, eve=None):
|
2022-10-10 01:59:44 +00:00
|
|
|
event_system.emit("get_current_state")
|
2022-07-16 07:33:25 +00:00
|
|
|
|
2022-09-29 22:22:33 +00:00
|
|
|
state = self._fm_state
|
2022-07-16 07:33:25 +00:00
|
|
|
self._event_message = None
|
|
|
|
|
|
|
|
GLib.idle_add(self._process_changes, (state))
|
|
|
|
|
|
|
|
def _process_changes(self, state):
|
|
|
|
if len(state.selected_files) == 1:
|
|
|
|
uri = state.selected_files[0]
|
|
|
|
path = state.tab.get_current_directory()
|
|
|
|
|
|
|
|
|
|
|
|
properties = self._set_ui_data(uri, path)
|
|
|
|
response = self._properties_dialog.run()
|
|
|
|
if response in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT]:
|
|
|
|
self._properties_dialog.hide()
|
|
|
|
|
|
|
|
self._update_file(properties)
|
|
|
|
self._properties_dialog.hide()
|
|
|
|
|
|
|
|
|
|
|
|
def _update_file(self, properties):
|
|
|
|
chmod_stat = self._get_check_boxes()
|
|
|
|
|
|
|
|
if chmod_stat is not properties.chmod_stat:
|
|
|
|
try:
|
|
|
|
print("\nNew chmod flags...")
|
|
|
|
print(f"Old: {''.join(properties.chmod_stat)}")
|
|
|
|
print(f"New: {chmod_stat}")
|
2022-07-16 18:29:19 +00:00
|
|
|
|
|
|
|
command = ["chmod", f"{chmod_stat}", properties.file_uri]
|
|
|
|
with subprocess.Popen(command, stdout=subprocess.PIPE) as proc:
|
|
|
|
result = proc.stdout.read().decode("UTF-8").strip()
|
|
|
|
print(result)
|
|
|
|
except Exception as e:
|
|
|
|
print(f"Couldn't chmod\nFile: {properties.file_uri}")
|
|
|
|
print( repr(e) )
|
|
|
|
|
|
|
|
|
|
|
|
owner = self._file_owner.get_text()
|
|
|
|
group = self._file_group.get_text()
|
|
|
|
if owner is not properties.file_owner or group is not properties.file_group:
|
|
|
|
try:
|
|
|
|
print("\nNew owner/group flags...")
|
|
|
|
print(f"Old:\n\tOwner: {properties.file_owner}\n\tGroup: {properties.file_group}")
|
|
|
|
print(f"New:\n\tOwner: {owner}\n\tGroup: {group}")
|
|
|
|
|
|
|
|
uid = pwd.getpwnam(owner).pw_uid
|
|
|
|
gid = grp.getgrnam(group).gr_gid
|
|
|
|
os.chown(properties.file_uri, uid, gid)
|
2022-07-16 07:33:25 +00:00
|
|
|
except Exception as e:
|
|
|
|
print(f"Couldn't chmod\nFile: {properties.file_uri}")
|
|
|
|
print( repr(e) )
|
|
|
|
|
|
|
|
|
|
|
|
def _set_ui_data(self, uri, path):
|
|
|
|
properties = Properties()
|
|
|
|
file_info = Gio.File.new_for_path(uri).query_info(attributes="standard::*,owner::*,time::access,time::changed",
|
|
|
|
flags=Gio.FileQueryInfoFlags.NONE,
|
|
|
|
cancellable=None)
|
|
|
|
|
|
|
|
is_symlink = file_info.get_attribute_as_string("standard::is-symlink")
|
|
|
|
properties.file_uri = uri
|
|
|
|
properties.file_target = file_info.get_attribute_as_string("standard::symlink-target") if is_symlink else ""
|
|
|
|
properties.file_name = file_info.get_display_name()
|
|
|
|
properties.file_location = path
|
|
|
|
properties.mime_type = file_info.get_content_type()
|
|
|
|
properties.file_size = self._sizeof_fmt(file_info.get_size())
|
|
|
|
properties.mtime = datetime.fromtimestamp( int(file_info.get_attribute_as_string("time::changed")) ).strftime("%A, %B %d, %Y %I:%M:%S")
|
|
|
|
properties.atime = datetime.fromtimestamp( int(file_info.get_attribute_as_string("time::access")) ).strftime("%A, %B %d, %Y %I:%M:%S")
|
|
|
|
properties.file_owner = file_info.get_attribute_as_string("owner::user")
|
|
|
|
properties.file_group = file_info.get_attribute_as_string("owner::group")
|
|
|
|
|
|
|
|
# NOTE: Read = 4, Write = 2, Exec = 1
|
|
|
|
command = ["stat", "-c", "%a", uri]
|
|
|
|
with subprocess.Popen(command, stdout=subprocess.PIPE) as proc:
|
|
|
|
properties.chmod_stat = list(proc.stdout.read().decode("UTF-8").strip())
|
|
|
|
owner = self._chmod_map[f"{properties.chmod_stat[0]}"]
|
|
|
|
group = self._chmod_map[f"{properties.chmod_stat[1]}"]
|
|
|
|
others = self._chmod_map[f"{properties.chmod_stat[2]}"]
|
|
|
|
|
|
|
|
self._reset_check_boxes()
|
|
|
|
self._set_check_boxes([["owner", owner], ["group", group], ["others", others]])
|
|
|
|
|
|
|
|
self._file_name.set_text(properties.file_name)
|
|
|
|
self._file_location.set_text(properties.file_location)
|
|
|
|
self._file_target.set_text(properties.file_target)
|
|
|
|
self._mime_type.set_label(properties.mime_type)
|
|
|
|
self._file_size.set_label(properties.file_size)
|
|
|
|
self._mtime.set_text(properties.mtime)
|
|
|
|
self._atime.set_text(properties.atime)
|
|
|
|
self._file_owner.set_text(properties.file_owner)
|
|
|
|
self._file_group.set_text(properties.file_group)
|
|
|
|
|
|
|
|
return properties
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_check_boxes(self):
|
|
|
|
perms = [[], [], []]
|
|
|
|
|
|
|
|
for i, target in enumerate(["owner", "group", "others"]):
|
|
|
|
for type in ["r", "w", "x"]:
|
|
|
|
is_active = self._builder.get_object(f"{target}_{type}").get_active()
|
|
|
|
if is_active:
|
|
|
|
perms[i].append(type)
|
|
|
|
|
|
|
|
digits = []
|
|
|
|
for perm in perms:
|
|
|
|
digits.append(self._chmod_map_counter[ ''.join(perm) ])
|
|
|
|
|
|
|
|
return ''.join(digits)
|
|
|
|
|
|
|
|
def _set_check_boxes(self, targets):
|
|
|
|
for name, target in targets:
|
|
|
|
for type in list(target):
|
|
|
|
obj = f"{name}_{type}"
|
|
|
|
self._builder.get_object(obj).set_active(True)
|
|
|
|
|
|
|
|
def _reset_check_boxes(self):
|
|
|
|
for target in ["owner", "group", "others"]:
|
|
|
|
for type in ["r", "w", "x"]:
|
|
|
|
self._builder.get_object(f"{target}_{type}").set_active(False)
|
|
|
|
|
|
|
|
def _sizeof_fmt(self, num, suffix="B"):
|
|
|
|
for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
|
|
|
|
if abs(num) < 1024.0:
|
|
|
|
return f"{num:3.1f} {unit}{suffix}"
|
|
|
|
num /= 1024.0
|
|
|
|
return f"{num:.1f} Yi{suffix}"
|