Python Forum
My first project - RAMDisk
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
My first project - RAMDisk
#1
Hi,

here's my first project - RAMDisk.

#!/usr/bin/env python3

import string
import json
import os
import gi
import socket
import errno
import sys

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk


class NameEntry(Gtk.Entry, Gtk.Editable):
    def __init__(self, *args, **kwargs):
        Gtk.Entry.__init__(self, *args, **kwargs)

    def do_insert_text(self, character, length, position):
        if character in (*string.ascii_letters, *string.digits):
            self.get_buffer().insert_text(position, character, length)
            return position + length
        return position


class SizeEntry(Gtk.Entry, Gtk.Editable):
    def __init__(self, *args, **kwargs):
        Gtk.Entry.__init__(self, *args, **kwargs)

    def do_insert_text(self, character, length, position):
        if position == 0 and character == '0':
            return position
        if character in string.digits:
            self.get_buffer().insert_text(position, character, length)
            return position + length
        return position


class CreateRamdiskWindow(Gtk.Window):
    def __init__(self, parent):
        Gtk.Window.__init__(self)

        self.props.default_width = 400
        self.set_resizable(False)
        self.set_border_width(15)
        self.set_transient_for(parent)
        self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)

        self.parent = parent
        self.parent.set_sensitive(False)
        self.parent.props.accept_focus = False
        self.parent.connect('configure-event', lambda widget, event: self.on_parent_configure())

        self.connect('key-release-event', lambda widget, event: self.on_key_released(event))
        self.connect('configure-event', lambda widget, event: self.on_configure())
        self.connect('destroy', lambda widget: self.close())

        header_bar = Gtk.HeaderBar()
        header_bar.props.title = 'Create New Ramdisk'
        self.set_titlebar(header_bar)

        cancel_btn = Gtk.Button(label='Cancel')
        cancel_btn.connect('clicked', lambda widget: self.close())
        header_bar.pack_start(cancel_btn)

        apply_btn = Gtk.Button(label='Apply')
        apply_btn.connect('clicked', lambda widget: self.on_apply_clicked())
        header_bar.pack_end(apply_btn)

        grid = Gtk.Grid()
        grid.set_row_spacing(12)
        self.add(grid)

        name_box = Gtk.Box()
        name_label = Gtk.Label(label='Name', xalign=1, width_chars=10.5)
        self.name_entry = NameEntry(max_length=10)
        self.name_entry.set_icon_from_icon_name(1, 'dialog-question')
        name_tooltip_text = 'Maximum of 10 characters.\n' \
                            'English letters and digits only.\n' \
                            'Each ramdisk has to have a unique name.'
        self.name_entry.set_icon_tooltip_text(1, name_tooltip_text)
        name_box.pack_start(name_label, False, True, 12)
        name_box.pack_start(self.name_entry, True, True, 0)
        grid.attach(name_box, 0, 0, 1, 1)

        size_box = Gtk.Box()
        size_label = Gtk.Label(label='Size', xalign=1, width_chars=10.5)
        self.size_entry = SizeEntry()
        self.size_entry.set_icon_from_icon_name(1, 'dialog-question')
        size_tooltip_text = "RAM usage equals the size of stored files.\n" \
                            "Ramdisk's size may exceed the physical memory,\n" \
                            "utilizing the swap partition when necessary."
        self.size_entry.set_icon_tooltip_text(1, size_tooltip_text)

        unit_store = Gtk.ListStore(str)
        units = ['MB', 'GB', 'TB']
        for unit in units:
            unit_store.append([unit])
        unit_combo = Gtk.ComboBox.new_with_model(unit_store)
        unit_combo.connect('changed', self.on_unit_changed)
        unit_combo.set_active(1)
        self.unit = 'GB'
        renderer_text = Gtk.CellRendererText()
        unit_combo.pack_start(renderer_text, True)
        unit_combo.add_attribute(renderer_text, 'text', 0)

        size_box.pack_start(size_label, True, True, 12)
        size_box.pack_start(self.size_entry, True, True, 0)
        size_box.pack_start(unit_combo, True, True, 2)
        grid.attach_next_to(size_box, name_box, Gtk.PositionType.BOTTOM, 1, 1)

    def on_key_released(self, event):
        if event.keyval == 65293:  # return
            self.on_apply_clicked()
        elif event.keyval == 65307:  # esc
            self.close()

    def on_apply_clicked(self):
        name = self.name_entry.get_text()
        size = self.size_entry.get_text()
        unit = self.unit

        if len(name) == 0 or len(size) == 0:
            dialog = Gtk.MessageDialog(
                transient_for=self,
                flags=0,
                message_type=Gtk.MessageType.INFO,
                buttons=Gtk.ButtonsType.OK,
                text='Info'.ljust(37),
            )
            dialog.format_secondary_text('Enter name and size'.ljust(30))
            dialog.run()
            dialog.destroy()
            return

        if db.exists(name):
            dialog = Gtk.MessageDialog(
                transient_for=self,
                flags=0,
                message_type=Gtk.MessageType.WARNING,
                buttons=Gtk.ButtonsType.OK,
                text='Error'.ljust(36),
            )
            dialog.format_secondary_text('Ramdisk exists'.ljust(36))
            dialog.run()
            dialog.destroy()
            return

        if os.path.exists(f'/mnt/{name}'):
            dialog = Gtk.MessageDialog(
                transient_for=self,
                flags=0,
                message_type=Gtk.MessageType.WARNING,
                buttons=Gtk.ButtonsType.OK,
                text='Error'.ljust(36),
            )
            dialog.format_secondary_text('Choose another name'.ljust(26))
            dialog.run()
            dialog.destroy()
            return

        create(name, size, unit)

        self.parent.render()
        self.close()

    def on_unit_changed(self, combo):
        tree_iter = combo.get_active_iter()
        if tree_iter is not None:
            model = combo.get_model()
            self.unit = model[tree_iter][0]

    def on_configure(self):
        if self.has_toplevel_focus():
            child_width, child_height = self.get_size()
            parent_width, parent_height = self.parent.get_size()
            width_diff, height_diff = parent_width - child_width, parent_height - child_height
            child_x, child_y = self.get_position()
            new_parent_position = (child_x - int(width_diff / 2), child_y - int(height_diff / 2))
            self.parent.move(*new_parent_position)

    def on_parent_configure(self):
        if self.parent.has_toplevel_focus():
            child_width, child_height = self.get_size()
            parent_width, parent_height = self.parent.get_size()
            width_diff, height_diff = parent_width - child_width, parent_height - child_height
            parent_x, parent_y = self.parent.get_position()
            new_child_position = (parent_x + int(width_diff / 2), parent_y + int(height_diff / 2))
            self.move(*new_child_position)

    def close(self):
        self.parent.set_sensitive(True)
        self.parent.props.accept_focus = True
        self.destroy()


class MainWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)

        self.set_resizable(False)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_focus_visible(False)
        self.props.default_height = 300
        self.set_border_width(15)
        self.set_icon_from_file('/usr/share/pixmaps/ramdisk.png')

        header_bar = Gtk.HeaderBar()
        header_bar.set_show_close_button(True)
        header_bar.props.title = 'RAMDisk'
        self.set_titlebar(header_bar)

        padding_label = Gtk.Label(width_chars=0.5)
        header_bar.pack_start(padding_label)

        new_btn = Gtk.Button.new_from_icon_name('list-add', Gtk.IconSize.MENU)
        new_btn.connect('clicked', lambda widget: CreateRamdiskWindow(self).show_all())
        header_bar.pack_start(new_btn)

        self.grid = Gtk.Grid()
        self.grid.set_row_spacing(10)
        self.add(self.grid)

        self.next_available_row = 0
        self.render()

    def render(self):
        while self.next_available_row > 0:
            self.grid.remove_row(0)
            self.next_available_row -= 1

        if db.count == 0:
            self.display_about()
        else:
            self.display_header_row()
            for name, size, unit in db.list:
                self.display_ramdisk(name, size, unit)

        self.show_all()

    def display_about(self):
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)

        img = Gtk.Image()
        img.set_from_file('/usr/share/pixmaps/ramdisk.png')

        name_label = Gtk.Label(label='RAMDisk', width_chars=58)
        version_label = Gtk.Label(label='1.0.0\n', width_chars=58)
        repo_label = Gtk.Label(width_chars=58)
        repo_label.set_markup('<a href="https://github.com/estarq/ramdisk">Github Repository</a>')
        license_label = Gtk.Label(label='Licensed under the MIT license', width_chars=58)
        copyright_label = Gtk.Label(label=u'Copyright \u00A9 2020 Paul Lloyd', width_chars=58)

        box.pack_start(img, True, True, 0)
        box.pack_start(name_label, True, True, 0)
        box.pack_start(version_label, True, True, 0)
        box.pack_start(repo_label, True, True, 0)
        box.pack_start(license_label, True, True, 0)
        box.pack_start(copyright_label, True, True, 0)

        self.grid.attach(box, 0, self.next_available_row, 1, 1)
        self.next_available_row += 1

    def display_header_row(self):
        box = Gtk.Box()

        name_label = Gtk.Label(width_chars=19)
        name_label.set_markup('<b>Name</b>')

        location_label = Gtk.Label(width_chars=20)
        location_label.set_markup('<b>Location</b>')

        size_label = Gtk.Label(width_chars=15)
        size_label.set_markup('<b>Size</b>')

        padding_label = Gtk.Label(width_chars=4)

        box.pack_start(name_label, True, True, 0)
        box.pack_start(location_label, True, True, 0)
        box.pack_start(size_label, True, True, 0)
        box.pack_start(padding_label, True, True, 0)

        self.grid.attach(box, 0, self.next_available_row, 1, 1)
        self.next_available_row += 1

    def display_ramdisk(self, name, size, unit):
        box = Gtk.Box()

        name_label = Gtk.Label(label=name, width_chars=19)
        location_label = Gtk.Label(label=f'/mnt/{name}', width_chars=20)
        size_label = Gtk.Label(label=f'{size} {unit}', width_chars=15)

        remove_ramdisk_btn = Gtk.Button.new_from_icon_name('list-remove', Gtk.IconSize.BUTTON)
        remove_ramdisk_btn.connect('clicked', self.on_remove_clicked, name)

        box.pack_start(name_label, True, True, 0)
        box.pack_start(location_label, True, True, 0)
        box.pack_start(size_label, True, True, 0)
        box.pack_start(remove_ramdisk_btn, False, True, 0)

        self.grid.attach(Gtk.Separator(), 0, self.next_available_row, 1, 1)
        self.grid.attach(box, 0, self.next_available_row + 1, 1, 1)
        self.next_available_row += 2

    def on_remove_clicked(self, widget, name):
        remove(name)
        self.render()


class DataStore:
    def __init__(self, json_file_path):
        self.path = json_file_path

        with open(self.path) as f:
            self.list = json.loads(json.load(f))

        self.count = len(self.list)

    def exists(self, name):
        for ramdisk in self.list:
            if name in ramdisk:
                return True
        return False

    def select(self, name):
        for ramdisk in self.list:
            if name in ramdisk:
                return ramdisk

    def insert(self, name, size, unit):
        self.list.append([name, size, unit])
        self.count += 1

        with open(self.path, 'w') as f:
            json.dump(json.dumps(self.list), f)

    def delete(self, name):
        self.list.remove(self.select(name))
        self.count -= 1

        with open(self.path, 'w') as f:
            json.dump(json.dumps(self.list), f)


def create(name, size, unit):
    os.mkdir(f'/mnt/{name}')

    mib_in_mb = 0.95367431640625
    multiplier = {'MB': 1, 'GB': 1000, 'TB': 1_000_000}
    size_mebibytes = int(multiplier[unit] * int(size) * mib_in_mb) + 1

    fstab_entry = f'{name} /mnt/{name} tmpfs defaults,size={size_mebibytes}M,x-gvfs-show 0 0'
    with open('/etc/fstab', 'a') as f:
        f.write(fstab_entry + '\n')
    os.system(f'mount /mnt/{name}')

    db.insert(name, size, unit)


def remove(name):
    fstab_entry_beginning = f'{name} /mnt/{name}'
    with open('/etc/fstab', 'r+') as f:
        fstab_entries = f.readlines()
        for idx, fstab_entry in enumerate(fstab_entries):
            if fstab_entry_beginning in fstab_entry:
                fstab_entries.pop(idx)
        f.seek(0)
        f.truncate()
        f.write(''.join(fstab_entries))

    os.system(f'umount --lazy /mnt/{name}')
    os.system(f'rm --recursive --force /mnt/{name}')

    db.delete(name)


if len(sys.argv) == 1:
    os.system('pkexec ramdisk --with-privileges')
    sys.exit(0)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    s.bind(('localhost', 60743))
except socket.error as err:
    if err.errno == errno.EADDRINUSE:
        warn_dialog = Gtk.MessageDialog(
            flags=0,
            message_type=Gtk.MessageType.WARNING,
            buttons=Gtk.ButtonsType.OK,
            text='Warning'.ljust(84),
        )
        warn_dialog.format_secondary_text('Using multiple instances of RAMDisk may lead to errors.')
        warn_dialog.run()
        warn_dialog.destroy()

db = DataStore('/usr/share/ramdisk/data.json')

window = MainWindow()
window.connect('destroy', Gtk.main_quit)
window.show_all()
Gtk.main()
I would love to know what you think about the code.

Paul
Larz60+ write Apr-06-2021, 09:42 PM:
Please post code here, Refer to BBCode help topic on how to post.

Many, including myself, will not follow links.
Reply
#2
I think @Larz60+ overlooked the fact that it was link to github repo and it is explicitly mentioned as an option in our help section:
Quote:If you feel you must post the full code and it is very long and/or has multiple files and/or resources to run, you need to put your program in a repo for us to obtain it instead of posting it in the forum and making us piece it all together

In this case you share complete project, so this is OK.
Larz60+ likes this post
If you can't explain it to a six year old, you don't understand it yourself, Albert Einstein
How to Ask Questions The Smart Way: link and another link
Create MCV example
Debug small programs

Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020