Статья Работа с облаком MailRu с помощью Python. Часть 02

Не будем ходить вокруг да около. В первой части данной статьи мы создали модуль для работы с облаком, который выполняет основные функции копирования, перемещения, скачивания, загрузки и некоторые другие. В этой же части мы продолжим начатое и создадим интерфейс приложения, а также код, который будет обрабатывать нажатия на кнопки в нем.

cloud_2.png

Наверное, начинать разработку любого приложения нужно с продумывания функционала, которое оно будет иметь, а также создания интерфейса для использования этого функционала. И уже в последнюю очередь кодировать взаимодействие интерфейса и функций, выполняющих основную работу. Хотя, может быть я и ошибаюсь. Тем не менее, для начала, так как функционал приложения у нас уже известен, его мы закодировали в модуле, который создали в предыдущей статье, настало время создать интерфейс приложения. Подробно останавливаться на этом я не буду. Так как интерфейс у каждого может получиться немного свой.


Что потребуется?

Для того, чтобы создать интерфейс приложения, а делать его мы будем в Qt Designer, его необходимо установить. Для Windows установить Дизайнер можно с помощью команды:

pip install pyqt5-tools

Если вы выполните данную команду в вашем виртуальном окружении, то найти исполняемый файл дизайнера можно будет по пути, для примера: C:\папка_с_кодом\venv\Lib\site-packages\qt5_applications\Qt\bin.
Или можно скачать инсталлятор с сайта: Переход по внешней ссылке

Установить Дизайнер для Linux на базе Debian можно с помощью выполнения нескольких команд. Последовательно вводим их в терминале:

Код:
sudo apt-get install python3-pyqt5
sudo apt-get install pyqt5-dev-tools
sudo apt-get install qttools5-dev-tools

После чего, ярлык Дизайнера можно будет найти в меню приложений.
Запускаем Дизайнер и делаем что-то похожее на картинку ниже:

03.png

Я попытался указать, что за виджет я использовал, а также имя для виджета, которое указал в приложении. Ничего особо сложного, в том, чтобы накидать такой интерфейс нет, потому на этом останавливаться не будем, двигаемся дальше.

На следующем шаге нам потребуется установить библиотеки, которые будут необходимы для работы нашего приложения. Так как интерфейс приложения мы делаем в Qt, значит нам понадобиться библиотека для работы с ним из python - PyQt5. Следующий модуль, который нужно будет установить – wmi. С помощью данного модуля мы будем получать серийный номер процессора, с помощью которого осуществлять шифрование-дешифровку логина и пароля для входа в облако, чтобы не хранить их в открытом виде. Данный модуль необходимо установить только тем, кто создает приложение в ОС Windows. В ОС Linux установка данного модуля не требуется. И библиотека cryptocode, которую мы уже устанавливали в предыдущей статье. Ее мы будем использовать для шифрования-дешифровки логина и пароля.

Для ОС Windows пишем в терминале команду:

pip install PyQt5 wmi cryptocode

Для ОС Linux:

pip install PyQt5 cryptocode

Создадим файл mail_cloud_webdev.py. Импортируем в него установленные библиотеки. Также импортируем модуль с интерфейсом приложения, который конвертируем из формы созданной Дизайнером в скрипт python с помощью команды:

pyuic5 mail_cloud.ui -o mail_cloud.py

Я назвал форму созданную в Дизайнере mail_cloud.ui.
Также импортируем в скрипт функции из модуля, который мы создали в предыдущей статье.

Python:
from platform import system as psys
from sys import exit as ext, argv
from pathlib import Path

from cryptocode import encrypt
from PyQt5.QtWidgets import QMessageBox, QAction, QDialog, QLineEdit, QDialogButtonBox, QFormLayout, QInputDialog

from mail_cloud import *
from mail_tools import connect_cloud_folder, create_cloud_folder, delete_cloud_folder, check_setting, authorize
from mail_tools import upload_cloud_file, download_cloud_file, move_cloud_object, copy_cloud_object


Создание класса интерфейса приложения

Для того, чтобы отобразить форму, которую мы создали в Дизайнере, необходимо создать класс приложения, который и будет отвечать за его отображение, а, после добавления в него необходимых методов, обрабатывать нажатия на кнопки.

Создадим класс class MyWin(QtWidgets.QMainWindow). Проведем его первоначальную инициализацию. Инициализируем виджеты формы. После проинициализируем несколько функций, которые будут создавать контекстное меню таблицы и обрабатывать нажатия на его пункты. Также поместим сюда словарь, куда будут складываться полученные папки и файлы из облака с указанием полного пути к ним, названия, является ли указанный пункт директорией. Создадим две переменные self.cut_object и self.copy_object, в которые будем помещать путь к папкам/файлам которые нужно скопировать или переместить. Также инициализируем переменную, куда будем помещать переданный из модуля работы с облаком, после авторизации, объект клиента. А далее инициализируем обработчики нажатий на кнопки приложения.

Python:
class MyWin(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        """
        Инициализация окна приложения, а также дополнительных
        переменных для работы скрипта.
        """
        QtWidgets.QWidget.__init__(self, parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        self.create_actions()
        self.create_context_menu()
        self.connect_action()

        self.files = dict()
        self.cut_object = None
        self.copy_object = None
        self.client = None

        self.ui.tableWidget.cellDoubleClicked.connect(self.folder_view)
        self.ui.create_Folder.clicked.connect(self.create_folder)
        self.ui.delete_Folder.clicked.connect(self.delete_folder)
        self.ui.uploadFile.clicked.connect(self.upload_file)
        self.ui.downloadFile.clicked.connect(self.download_file)
        self.ui.exitButton.clicked.connect(self.exit_application)
        self.ui.tableWidget.customContextMenuRequested[QtCore.QPoint].connect(self.create_actions)


Создание контекстного меню таблицы

В нашем приложении присутствует таблица, в которой отображаются папки и файлы присутствующие в директории облака. Для того, чтобы копировать или перемещать данные объекты я решил, что будет удобнее сделать контекстное меню таблицы, которое и позволит это сделать. Поэтому, в данном меню будут реализованы три пункта: копировать, вырезать и вставить. Ну и добавим между ними разделитель.

Для начала создадим пункты меню. Для этого создаем функцию def create_actions(self).

Python:
    def create_actions(self):
        """
        Создание пунктов меню.
        """
        self.copyAction = QAction("&Copy", self)
        self.cutAction = QAction("C&ut", self)
        self.pasteAction = QAction("&Paste", self)
        self.separator = QAction(self)
        self.separator.setSeparator(True)

В ней мы создаем каждый пункт, включая разделитель.
После того, как меню создано, необходимо добавить его в нужный нам виджет, а именно в виджет таблицы. Поэтому создаем функцию def create_context_menu(self). После чего, с помощью политики контекстного меню добавляем его в таблицу.

Python:
    def create_context_menu(self):
        """
        Создание контекстного меню.
        """
        self.ui.tableWidget.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.ActionsContextMenu)
        self.ui.tableWidget.addAction(self.copyAction)
        self.ui.tableWidget.addAction(self.cutAction)
        self.ui.tableWidget.addAction(self.separator)
        self.ui.tableWidget.addAction(self.pasteAction)

Для того, чтобы обрабатывались действия при выборе того или иного пункта, нужно указать для них триггер, то есть, связать каждый пункт меню с определенной функцией. Создадим функцию def connect_action(self). И присвоим каждому пункту определенный триггер связывающий пункт меню с обработчиком. Обработчики, то есть функции указанные здесь, мы создадим ниже.

Python:
    def connect_action(self):
        """
        Создание действий при выборе пунктов меню.
        """
        self.copyAction.triggered.connect(self.copy_content)
        self.pasteAction.triggered.connect(self.paste_content)
        self.cutAction.triggered.connect(self.cut_content)

Создадим обработчик события при нажатии на пункт меню «Cut» - вырезать (переместить). Так как мы выполняем перемещение папки или файла, необходимо, чтобы переменная, в которой содержится путь, если объект копируется, была пуста. Поэтому, присваиваем ей значение None. Это необходимо потому, что функция, обрабатывающая вставку папки или файла после копирования или перемещения – одна для обоих этих действий. Далее, считываем индекс ячейки в таблице. Копируем из нее текст. И помещаем в переменную self.cut_object путь, который получаем в свою очередь из словаря по тексту, который мы забрали из ячейки.

Python:
    def cut_content(self):
        """
        Действие выполняющееся при выборе пункта "Вырезать"
        """
        self.copy_object = None
        row = self.ui.tableWidget.currentRow()
        text = self.ui.tableWidget.item(row, 0).text()
        self.cut_object = self.files.get(text).get("path")

Обработчик нажатия на пункт меню «Copy» - копировать точно такой же, как и предыдущий обработчик. Единственное различие заключается в том, что в этом случае значение None присваивается переменной self.cut_object, а путь к копируемому файлу в переменную self.copy_object.

Python:
    def copy_content(self):
        """
        Действие выполняющееся при выборе пункта "Копировать"
        """
        self.cut_object = None
        row = self.ui.tableWidget.currentRow()
        text = self.ui.tableWidget.item(row, 0).text()
        self.copy_object = self.files.get(text).get("path")

Создадим обработчик для пункта меню «Paste» - вставить. Вначале проверяем, какая из переменных не является пустой: перемещения или копирования. Получаем путь к объекту, который нужно скопировать – переместить. Запускаем функцию копирования или перемещения в которые передаем объект клиента, путь к перемещаемому – копируемому объекту, путь, куда этот объект нужно вставить. Затем перечитываем содержимое директории, куда производилось перемещение – копирование, чтобы отобразить изменения в таблице. И далее – присваиваем переменной содержащей путь к копируемому – перемещаемому объекту значение None, во избежание накладок в будущем.

Python:
    def paste_content(self):
        """
        Обработка действия при выборе пункта меню "Вставить".
        В зависимости от того, в какой переменной содержаться данные.
        """
        if self.cut_object:
            paste_object = self.get_paste_object(self.cut_object)
            move_cloud_object(self.client, self.cut_object, paste_object)
            self.connect_cloud(self.ui.pathEdit.text().split("/")[-1])
            self.cut_object = None
        if self.copy_object:
            paste_object = self.get_paste_object(self.copy_object)
            copy_cloud_object(self.client, self.copy_object, paste_object)
            self.connect_cloud(self.ui.pathEdit.text().split("/")[-1])
            self.copy_object = None

И еще одна функция, в которой мы будем получать путь к копируемому – перемещаемому объекту def get_paste_object(self, cc_object: str) -> str. Передаем в функцию значение переменной (в зависимости от контекста), забираем путь, который содержится в Edit и если после его разбивки он не равен «», то присваиваем переменной paste_object путь со слешем. Если же он пуст, это значит, что перемещение – копирование происходит в корень диска, а значит, путь здесь будет просто «/». Он также содержится в Edit. Потому, забираем его оттуда.

После, возвращаем переменную paste_object из функции.

Python:
    def get_paste_object(self, cc_object: str) -> str:
        """
        Получение пути к объекту копирования - перемещения.
        """
        if self.ui.pathEdit.text().split("/")[-1]:
            paste_object = f'{self.ui.pathEdit.text()}/{cc_object.split("/")[-1]}'
        else:
            paste_object = f'{self.ui.pathEdit.text()}{cc_object.split("/")[-1]}'
        return paste_object


Завершение работы приложения

Создадим маленькую функцию, единственным предназначением которой будет обрабатывать нажатие на кнопку «Выход». Так как она не работает с другими функциями или методами класса, сделаем ее статической.

Python:
    @staticmethod
    def exit_application():
        """
        Завершение работы приложения.
        """
        QtWidgets.QApplication.quit()


Скачивание файла/папки из Облака

Создадим функцию def download_file(self). Она будет обрабатывать нажатие на кнопку «Скачать». Для начала получаем индекс ячейки. Забираем из ячейки по полученному индексу текст. Если вы при этом не выделите мышкой ячейку, значение text будет «…». А следовательно, скачивать ничего не нужно. Поэтому проверим, не равно ли значение переменной text «…». Если нет, запускаем диалог выбора папки для загрузки. Получаем из него путь к выбранной папке. Создаем полный локальный путь для загрузки путем объединения полученной папки и переменной text, в которой храниться название файла или директории для загрузки. Теперь получим полный путь к фалу/папке в облаке. Для этого заберем значение из Edit, проверим, не равно ли оно пустому значению после разбивки по «/». Если нет, объединяем содержимое Edit и переменной текст. Если равно, значит загружаемый файл находится в корне облака. Следовательно, путь к нему будет немного иной. После отправляем полученные значения в функцию загрузки, куда передаем клиента, путь к папке в облаке, локальный путь. Если из функции возвратилось True, выводим сообщение, что загрузка завершена.

Python:
    def download_file(self):
        """
        Обработка нажатия на кнопку Скачать.
        """
        row = self.ui.tableWidget.currentRow()
        text = self.ui.tableWidget.item(row, 0).text()
        if text != "...":
            directory = QtWidgets.QFileDialog.getExistingDirectory(None, "Selecting a folder to save in", ".")
            local_path = Path(directory) / text
            if self.ui.pathEdit.text().split("/")[-1]:
                remote_path = f'{self.ui.pathEdit.text()}/{text}'
            else:
                remote_path = f'/{text}'
            if download_cloud_file(self.client, remote_path, local_path):
                QMessageBox.warning(self, "Сообщение", "Загрузка завершена")


Загрузка файла/папки в Облако

Создадим функцию def upload_file(self). Откроем диалог выбора фалов или папок, или того и другого вместе, выберем нужные файлы. Из диалога вернется список с путями к выбранным объектам. Дело в том, что стандартной функции открытия именно в требующемся ключе, чтобы можно было выбрать несколько файлов и папок одновременно, в Qt нет. Хотя, может быть я и ошибаюсь. Тем не менее, существует функция для выбора нескольких фалов. Но, она не подходит в данном случае. Поэтому, так как у меня ума пока не хватает, написать данную функцию самостоятельно, я нашел реализация на Stack Overflow. Это, на всякий случай, к вопросу о том, что где ее уже видели. Впрочем, после того, как я немного в ней разобрался, я понял, что и как работает. Двигаемся дальше.

Итерируемся по полученному списку с путями к папкам/файлам. Получаем для них, по очереди путь для загрузки в облаке, локальным путь уже будет содержатся в переменной fil. Передаем в функцию загрузки клиента, путь в облаке, путь к файлу/папке для загрузки. После запускаем функцию получения списка файлов в директории облака, куда передаем путь к этой директории. Это необходимо для обновления информации в таблице.

После загрузки всех объектов выводим сообщение о завершении выгрузки.

Python:
    def upload_file(self):
        """
        Обработка нажатия на клавишу Загрузить.
        """
        files = self.getOpenFilesAndDirs(None, "Выбор файла", "", "*.* (*.*)")
        if files:
            for fil in files:
                if self.ui.pathEdit.text().split("/")[-1]:
                    remote_path = f'{self.ui.pathEdit.text()}/{Path(str(fil)).name}'
                    path = self.ui.pathEdit.text().split("/")[-1]
                else:
                    remote_path = f'{self.ui.pathEdit.text()}{Path(str(fil)).name}'
                    path = "..."
                upload_cloud_file(self.client, remote_path, fil)
                self.connect_cloud(path)
            QMessageBox.warning(self, "Сообщение", "Выгрузка завершена")


Удаление файлов/папок в Облаке

Создадим функцию def delete_folder(self). С ее помощью мы будем обрабатывать нажатие на кнопку «Удалить». Здесь все как и раньше. Получаем индекс ячейки в таблице. Забираем из нее текст. Получаем по тексту из словаря путь в облаке к файлу/папке, которые необходимо удалить. Передаем клиента и путь в функцию удаления. После чего обновляем содержимое таблицы и удаляем из словаря данные об удаленном файле/папке из словаря. Также обработаем исключение, так как иногда можно получить, если не выбрана ячейка, ошибку AttributeError. В этом случае просто ничего не будем делать.

Python:
    def delete_folder(self):
        """
        Обработка нажатия на кнопку Удалить.
        """
        try:
            row = self.ui.tableWidget.currentRow()
            text = self.ui.tableWidget.item(row, 0).text()
            path = self.files.get(text).get("path")
            delete_cloud_folder(self.client, path)
            if self.ui.pathEdit.text().split("/")[-1]:
                self.connect_cloud(self.ui.pathEdit.text().split("/")[-1])
            else:
                self.connect_cloud("...")
            self.files.pop(text)
        except AttributeError:
            pass


Создание папки в Облаке

Создадим функцию для создания папки в облаке. Для начала получим директорию, где будем создавать папку. Самый простой и быстрый путь – забрать значение из Edit, чем мы и воспользуемся. Выведем диалог для ввода имени создаваемой папки. Проверяем, было ли введено значение пользователем. Если да, то в tr будет содержаться True. Следовательно, проверяем, чему равен путь в облаке, для того, чтобы корректно составить путь для передачи в функцию создания папки. И далее, передаем в функцию создания папки клиента, и полный путь к создаваемой папке. Если возвращается True, обновляем словарь и добавляем в него еще одну, только что созданную директорию, после чего передаем ее в функцию получения данных о содержимом только что созданной директории, чтобы обновить таблицу. Выглядеть это будет так, как будто мы сразу же открыли ее после создания. Если имя папки введено не было, просто ничего не делаем.

Python:
    def create_folder(self):
        """
        Обработка нажатия на кнопку "Создать папку".
        """
        folder = self.ui.pathEdit.text()
        input_name, tr = QtWidgets.QInputDialog.getText(self, 'Создать папку', 'Введите имя папки:')
        if tr:
            if folder == "/":
                name = f'/{input_name}'
            else:
                name = "/".join([folder, input_name])
            if create_cloud_folder(self.client, name):
                self.files.update({input_name: {"path": f'{name}', "isdir": True}})
                self.connect_cloud(input_name)
        else:
            pass


Обработка двойного клика в ячейке таблицы. Просмотр содержимого папки

Создадим функцию для просмотра содержимого папок в облаке. В данном случае мы будем обрабатывать на нажатие на кнопку, а двойной клик в ячейке таблицы. Поэтому, получаем индекс ячейки. Забираем из данной ячейки текст. Проверяем, является ли выбранный объект директорией. Если да, передаем путь в функцию для получения списка папок/файлов, тем самым обновляя таблицу. Если же файл по которому мы кликнули не является директорией, тогда путь к нему передадим в функцию загрузки, скачаем его и выведем об этом сообщение. В этом случае файл будет скачан в директорию с приложением.

Python:
    def folder_view(self):
        """
        Обработка двойного клика в таблице приложения.
        """
        row = self.ui.tableWidget.currentRow()
        text = self.ui.tableWidget.item(row, 0).text()
        if self.files.get(text).get("isdir"):
            self.connect_cloud(text)
        else:
            download_cloud_file(self.client, self.files.get(text).get("path"), str(Path.cwd() / text))
            QMessageBox.warning(self, "Сообщение", "Загрузка завершена в текущую директорию")


Получение словаря с содержимым папки в Облаке

Создадим функцию, которую мы уже использовали не раз в для получения содержимого папки в облаке. Для начала проверяем, не пуст ли словарь с данными о содержимом папки. Если нет, получаем из словаря по переданному в функцию ключу путь к директории, которую нужно прочитать. Передаем путь в функцию для получения словаря с данными о содержимом директории, также в нее передаем клиента. Далее формируем переменную path. Это путь, который мы будем передавать дальше, в функцию обновления данных в таблице. Проверяем, не является ли он пустым после формирования. Если нет, передаем полученный словарь и путь. Если путь пуст, передаем словарь и в качестве пути «/».

Если же словарь с данными пуст изначально, устанавливаем в Edit путь из переданной в функцию переменной path, получаем словарь с данными о содержимом переданной директории, после чего, передаем словарь и содержимое path в функцию обновления данных в таблице.

Python:
    def connect_cloud(self, path: str):
        """
        Получение словаря с данными о папках и файлах в выбранной директории облака.
        """
        if self.files:
            self.ui.pathEdit.setText(self.files.get(path).get("path"))
            files = connect_cloud_folder(self.client, self.files.get(path).get("path"))
            path = "/".join(self.files.get(path).get("path").split("/")[:-1])
            if path:
                self.table_construct(files, path)
            else:
                self.table_construct(files, "/")
        else:
            self.ui.pathEdit.setText(path)
            files = connect_cloud_folder(self.client, path)
            self.table_construct(files, path)


Обновление данных в таблице о содержимом папки в Облаке

Создадим функцию, с помощью которой будем обновлять данные в таблице о содержимом папки в облаке. Для этого, сначала удалим из таблицы все значения. Получим текущую позицию. Добавим в текущую позицию строку. И запишем в созданную строку значение «…», после чего добавим данные об этом в словарь self.files. Это необходимо, чтобы создать путь, по которому можно перейти в каталог выше.

Далее итерируемся по полученным ключам в словаре files. Получаем текущую позицию. Создаем строку. Добавляем в созданную строку имя файла или папки, обновляем словарь, в котором ключом служит имя фала или папки, а значением является словарь, который содержит полный путь к папки или файлу, является ли файл директорией.

После того, как итерация будет завершена, устанавливаем ширину таблицы в соответствии с шириной ее содержимого. Также устанавливаем триггер, чтобы сделать таблицу не редактируемой. Это необходимо, чтобы корректно обрабатывать двойной клик мышью.

Python:
    def table_construct(self, files: list, path: str):
        """
        Заполнение таблицы данными полученными в запросе к облаку.
        """
        self.ui.tableWidget.setRowCount(0)
        row_position = self.ui.tableWidget.rowCount()
        self.ui.tableWidget.insertRow(row_position)
        self.ui.tableWidget.setItem(row_position, 0, QtWidgets.QTableWidgetItem("..."))
        self.files.update({"...": {"path": path, "isdir": True}})
        for path in files:
            row_position = self.ui.tableWidget.rowCount()
            self.ui.tableWidget.insertRow(row_position)
            self.ui.tableWidget.setItem(row_position, 0, QtWidgets.QTableWidgetItem(path.get("name")))
            self.files.update({path.get("name"): {"path": path.get("path"), "isdir": path.get("isdir")}})
        self.ui.tableWidget.resizeColumnsToContents()
        self.ui.tableWidget.setEditTriggers(QtWidgets.QTableWidget.EditTrigger.NoEditTriggers)


Создание нестандартного диалогового окна для выбора папок и файлов

Так как данная функция не взаимодействует с методами класса, сделаем ее статической. В данную функцию будем передавать все атрибуты, которым присвоим значение по умолчанию, которые передаем в обычное диалоговое окно. Создаем фукнцию, которая будет получать путь переданный в функцию и добавлять его в словарь selected. Создаем диалог. Проверяем опции переданные в основную функцию. В зависимости от их наличия устанавливаем соответствующие значения в текущий диалог. В зависимости от выбранных папок или файлов добавляем их в строку выбора, которая отображается в нижней части окна и является LineEdit. После нажатия на кнопку «Open» возвращаем из функции значение списка с путями к файлам.

Python:
    @staticmethod
    def getOpenFilesAndDirs(parent=None, caption='', directory='', filter='', initialFilter='', options=None) -> list:
        """
        Создание окна для выбора файлов или папок для загрузки.
        """
        def updateText():
            selected = []
            for index in view.selectionModel().selectedRows():
                selected.append('"{}"'.format(index.data()))
            lineEdit.setText(' '.join(selected))

        dialog = QtWidgets.QFileDialog(parent, windowTitle=caption)
        dialog.setFileMode(dialog.ExistingFiles)
        if options:
            dialog.setOptions(options)
        dialog.setOption(dialog.DontUseNativeDialog, True)
        if directory:
            dialog.setDirectory(directory)
        if filter:
            dialog.setNameFilter(filter)
            if initialFilter:
                dialog.selectNameFilter(initialFilter)

        dialog.accept = lambda: QtWidgets.QDialog.accept(dialog)

        stackedWidget = dialog.findChild(QtWidgets.QStackedWidget)
        view = stackedWidget.findChild(QtWidgets.QListView)
        view.selectionModel().selectionChanged.connect(updateText)

        lineEdit = dialog.findChild(QtWidgets.QLineEdit)
        dialog.directoryEntered.connect(lambda: lineEdit.setText(''))

        dialog.exec_()
        return dialog.selectedFiles()

на страницу где я взял данную функцию.


Диалоговое окно ввода пароля

Данное диалоговое окно будет необходимо в том случае, если у вас ОС Linux, либо не удалось получить серийный номер процессора. Будьте внимательны, если вы используете Linux, вводить пароль придется постоянно. Этот пароль нужен для расшифровки зашифрованного логина и пароля для доступа к Облаку.

Создаем диалоговое окно. Запрашиваем ввод пароля. Если пользователь ввел данные – возвращаем их из функции. Если нет, возвращаем False.

Python:
    def input_pass(self):
        """
        Диалоговое окно для ввода пароля для шифрования логина и пароля для облака.
        """
        text, ok = QInputDialog.getText(self, 'Ввод пароля', 'Введите пароль:')
        return text if ok else False

На этом с функциями созданного класса приложения можно завершить. Теперь перейдем к созданию других функций, которые используются при запуске приложения.


Окно ввода данных авторизации в Облаке

Приложение, которые мы создаем, для авторизации в Облаке использует логин и пароль. Эти данные в зашифрованном виде сохраняются в файл setting.ini. Однако, для того, чтобы их туда поместить, их нужно запросить. А потому, нам необходимо диалоговое окно с двумя полями ввода. Так как стандартных окон именно такого вида в Qt не наблюдается, нам будет необходимо его создать.

Создадим дочерний класс class InputDialog(QDialog). Инициализируем его. Создадим два поля ввода, а также область с кнопками ввода. Поместим это в созданную форму. Создадим две подписи, которые привяжем к созданным полям ввода.
Создадим функцию, которая будет возвращать кортеж с содержимым полей ввода при нажатии кнопки «ОК». И пустой кортеж, если нажата кнопка «Cancel».

Python:
class InputDialog(QDialog):
    """
    Создание диалога с двумя полями ввода для логина и пароля облака.
    """
    def __init__(self, parent=None):
        super().__init__(parent)

        self.first = QLineEdit(self)
        self.second = QLineEdit(self)
        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)

        layout = QFormLayout(self)
        layout.addRow("Cloud Mail Login", self.first)
        layout.addRow("Cloud Mail Password", self.second)
        layout.addWidget(button_box)

        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)

    def get_inputs(self) -> tuple:
        return self.first.text(), self.second.text()


Получение серийного номера процессора

Создадим функцию для получения серийного номера процессора. Для этого в функции будем импортировать объект WMI. Это необходимо для того, чтобы приложение могло работать как в Windows, так и в Linux. Затем получим серийный номер. Если он был получен, вернем его из функции. В противном случае вернем False.

Python:
def cpu() -> (str, bool):
    """
    Получение серийного номера процессора для шифрования, который
    будет использоваться в Windows вместо пароля для шифрования.
    """
    from wmi import WMI
    cp = WMI().Win32_Processor()[0].ProcessorId
    return cp if cp else False


Функция шифрования данных авторизации

Создадим функцию для шифрования логина и пароля для авторизации в облаке. Для этого используем функцию encrypt из библиотеки cryptocode. Передадим в нее слово для шифрования, а также пароль, которым мы это слово шифруем. После шифрования вернем зашифрованное содержимое из функции.

Python:
def encrypt_word(word: str, pw: str) -> str:
    """
    Шифрование логина и пароля.
    """
    return encrypt(word, pw)


Инициализация приложения. Запрос пароля, запрос данных авторизации

Создадим условие:

Python:
if __name__ == '__main__':

Далее, активируем виджеты и поместим их на форму приложения.

Python:
    app = QtWidgets.QApplication(argv)
    myapp = MyWin()

После этого создадим переменную в которую будем помещать пароль для шифрования. Проверяем версию ОС. Если это Windows, пробуем получить серийный номер процессора. Именно он будет выступать паролем для шифрования. И в дальнейшем ввод пароля на данной машине не потребуется. Если серийный номер получить не удалось, вызываем диалоговое окно, в котором запрашиваем ввод пароля вручную. Если пользователь ничего не ввел, выходим из приложения. Если ОС Linux, в этом случае диалоговое окно для ввода пароля вызывается сразу.

Python:
    psw = None
    if psys() == "Windows":
        psw = cpu()
        if not psw:
            psw = myapp.input_pass()
            if not psw:
                ext(0)
    elif psys() == "Linux":
        psw = myapp.input_pass()
        if not psw:
            ext(0)

Теперь делаем проверку, существует ли файл setting.ini. Если не существует, запрашиваем у пользователя данные для входа в облако. Проверяем, ввел ли пользователь все данные. Если нет, сообщаем об этом и выходим из приложения. Если все было введено корректно, записываем в файл setting.ini зашифрованные логин и пароль. Ну и, если диалог был просто завершен, выходим из приложения.

Python:
    if not check_setting():
        dialog = InputDialog()
        if dialog.exec():
            login, password = dialog.get_inputs()
            if not login or not password:
                print("Данные для входа в Облако не получены")
                sys.exit(0)
            with open("setting.ini", "w", encoding="utf-8") as file:
                file.write(f'{encrypt_word(login, psw)}\n')
                file.write(f'{encrypt_word(password, psw)}\n')
        else:
            ext(0)

Теперь авторизуемся на основании полученного пароля и введенных данных. Или, если файл setting.ini существует, на основании парсинга данного файла. Проверяем, если авторизация прошла успешно, присваиваем переменной client объект клиента, который вернулся из функции авторизации. Читаем содержимое корневого каталога в Облаке. И запускаем окно приложения. Если же авторизация не удалась, выводим соответствующее сообщение.

Python:
    client = authorize(psw)
    if client:
        myapp.client = client
        myapp.connect_cloud(path="/")
        myapp.show()
        ext(app.exec_())
    else:
        print("Авторизация на удалась. Проверьте файл setting.ini")

Python:
if __name__ == '__main__':
    """
    Проверка наличия файла с настройками. Если нет, запрашиваем
    логин и пароль, шифруем и сохраняем в файл.
    Проверяем, если ОС Windows используем для шифрования серийный
    номер процессора. Если получить не удалось, запрашиваем у пользователя
    пароль для шифрования введенных данных для входа в облако.
    Если ОС Linux, запрашиваем у пользователя пароль для шифрования данных
    для входа в облако.
    Авторизация в облаке с полученным серийным номером или введенным паролем.
    Запуск приложения.
    """
    app = QtWidgets.QApplication(argv)
    myapp = MyWin()
    psw = None
    if psys() == "Windows":
        psw = cpu()
        if not psw:
            psw = myapp.input_pass()
            if not psw:
                ext(0)
    elif psys() == "Linux":
        psw = myapp.input_pass()
        if not psw:
            ext(0)
    if not check_setting():
        dialog = InputDialog()
        if dialog.exec():
            login, password = dialog.get_inputs()
            if not login or not password:
                print("Данные для входа в Облако не получены")
                sys.exit(0)
            with open("setting.ini", "w", encoding="utf-8") as file:
                file.write(f'{encrypt_word(login, psw)}\n')
                file.write(f'{encrypt_word(password, psw)}\n')
        else:
            ext(0)
    client = authorize(psw)
    if client:
        myapp.client = client
        myapp.connect_cloud(path="/")
        myapp.show()
        ext(app.exec_())
    else:
        print("Авторизация на удалась. Проверьте файл setting.ini")

Что же, а на этом, можно разработку данного приложения завершить. Конечно же, здесь можно доработать множество самых разнообразных функций, которые если подумать, обязательно найдутся. Но, это не для сегодняшней статьи.
Работу приложения я показывал в прошлой статье на видео.

Python:
"""
Приложение для работы с облаком. Требует для работы следующие библиотеки:
pip install PyQt5 pyqt5-tools wmi cryptocode
"""
import sys
from platform import system as psys
from sys import exit as ext, argv
from pathlib import Path

from cryptocode import encrypt
from PyQt5.QtWidgets import QMessageBox, QAction, QDialog, QLineEdit, QDialogButtonBox, QFormLayout, QInputDialog

from mail_cloud import *
from mail_tools import connect_cloud_folder, create_cloud_folder, delete_cloud_folder, check_setting, authorize
from mail_tools import upload_cloud_file, download_cloud_file, move_cloud_object, copy_cloud_object


class MyWin(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        """
        Инициализация окна приложения, а также дополнительных
        переменных для работы скрипта.
        """
        QtWidgets.QWidget.__init__(self, parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        self.create_actions()
        self.create_context_menu()
        self.connect_action()

        self.files = dict()
        self.cut_object = None
        self.copy_object = None
        self.client = None

        self.ui.tableWidget.cellDoubleClicked.connect(self.folder_view)
        self.ui.create_Folder.clicked.connect(self.create_folder)
        self.ui.delete_Folder.clicked.connect(self.delete_folder)
        self.ui.uploadFile.clicked.connect(self.upload_file)
        self.ui.downloadFile.clicked.connect(self.download_file)
        self.ui.exitButton.clicked.connect(self.exit_application)
        self.ui.tableWidget.customContextMenuRequested[QtCore.QPoint].connect(self.create_actions)

    # context menu begin
    def create_actions(self):
        """
        Создание пунктов меню.
        """
        self.copyAction = QAction("&Copy", self)
        self.cutAction = QAction("C&ut", self)
        self.pasteAction = QAction("&Paste", self)
        self.separator = QAction(self)
        self.separator.setSeparator(True)

    def create_context_menu(self):
        """
        Создание контекстного меню.
        """
        self.ui.tableWidget.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.ActionsContextMenu)
        self.ui.tableWidget.addAction(self.copyAction)
        self.ui.tableWidget.addAction(self.cutAction)
        self.ui.tableWidget.addAction(self.separator)
        self.ui.tableWidget.addAction(self.pasteAction)

    def connect_action(self):
        """
        Создание действий при выборе пунктов меню.
        """
        self.copyAction.triggered.connect(self.copy_content)
        self.pasteAction.triggered.connect(self.paste_content)
        self.cutAction.triggered.connect(self.cut_content)

    def cut_content(self):
        """
        Действие выполняющееся при выборе пункта "Вырезать"
        """
        self.copy_object = None
        row = self.ui.tableWidget.currentRow()
        text = self.ui.tableWidget.item(row, 0).text()
        self.cut_object = self.files.get(text).get("path")

    def copy_content(self):
        """
        Действие выполняющееся при выборе пункта "Копировать"
        """
        self.cut_object = None
        row = self.ui.tableWidget.currentRow()
        text = self.ui.tableWidget.item(row, 0).text()
        self.copy_object = self.files.get(text).get("path")

    def get_paste_object(self, cc_object: str) -> str:
        """
        Получение пути к объекту копирования - перемещения.
        """
        if self.ui.pathEdit.text().split("/")[-1]:
            paste_object = f'{self.ui.pathEdit.text()}/{cc_object.split("/")[-1]}'
        else:
            paste_object = f'{self.ui.pathEdit.text()}{cc_object.split("/")[-1]}'
        return paste_object

    def paste_content(self):
        """
        Обработка действия при выборе пункта меню "Вставить".
        В зависимости от того, в какой переменной содержаться данные.
        """
        if self.cut_object:
            paste_object = self.get_paste_object(self.cut_object)
            move_cloud_object(self.client, self.cut_object, paste_object)
            self.connect_cloud(self.ui.pathEdit.text().split("/")[-1])
            self.cut_object = None
        if self.copy_object:
            paste_object = self.get_paste_object(self.copy_object)
            copy_cloud_object(self.client, self.copy_object, paste_object)
            self.connect_cloud(self.ui.pathEdit.text().split("/")[-1])
            self.copy_object = None
    # context menu end

    @staticmethod
    def exit_application():
        """
        Завершение работы приложения.
        """
        QtWidgets.QApplication.quit()

    def download_file(self):
        """
        Обработка нажатия на кнопку Скачать.
        """
        row = self.ui.tableWidget.currentRow()
        text = self.ui.tableWidget.item(row, 0).text()
        if text != "...":
            directory = QtWidgets.QFileDialog.getExistingDirectory(None, "Selecting a folder to save in", ".")
            local_path = Path(directory) / text
            if self.ui.pathEdit.text().split("/")[-1]:
                remote_path = f'{self.ui.pathEdit.text()}/{text}'
            else:
                remote_path = f'/{text}'
            if download_cloud_file(self.client, remote_path, local_path):
                QMessageBox.warning(self, "Сообщение", "Загрузка завершена")

    def upload_file(self):
        """
        Обработка нажатия на клавишу Загрузить.
        """
        files = self.getOpenFilesAndDirs(None, "Выбор файла", "", "*.* (*.*)")
        if files:
            for fil in files:
                if self.ui.pathEdit.text().split("/")[-1]:
                    remote_path = f'{self.ui.pathEdit.text()}/{Path(str(fil)).name}'
                    path = self.ui.pathEdit.text().split("/")[-1]
                else:
                    remote_path = f'{self.ui.pathEdit.text()}{Path(str(fil)).name}'
                    path = "..."
                upload_cloud_file(self.client, remote_path, fil)
                self.connect_cloud(path)
            QMessageBox.warning(self, "Сообщение", "Выгрузка завершена")

    def delete_folder(self):
        """
        Обработка нажатия на кнопку Удалить.
        """
        try:
            row = self.ui.tableWidget.currentRow()
            text = self.ui.tableWidget.item(row, 0).text()
            path = self.files.get(text).get("path")
            delete_cloud_folder(self.client, path)
            if self.ui.pathEdit.text().split("/")[-1]:
                self.connect_cloud(self.ui.pathEdit.text().split("/")[-1])
            else:
                self.connect_cloud("...")
            self.files.pop(text)
        except AttributeError:
            pass

    def create_folder(self):
        """
        Обработка нажатия на кнопку "Создать папку".
        """
        folder = self.ui.pathEdit.text()
        input_name, tr = QtWidgets.QInputDialog.getText(self, 'Создать папку', 'Введите имя папки:')
        if tr:
            if folder == "/":
                name = f'/{input_name}'
            else:
                name = "/".join([folder, input_name])
            if create_cloud_folder(self.client, name):
                self.files.update({input_name: {"path": f'{name}', "isdir": True}})
                self.connect_cloud(input_name)
        else:
            pass

    def folder_view(self):
        """
        Обработка двойного клика в таблице приложения.
        """
        row = self.ui.tableWidget.currentRow()
        text = self.ui.tableWidget.item(row, 0).text()
        if self.files.get(text).get("isdir"):
            self.connect_cloud(text)
        else:
            download_cloud_file(self.client, self.files.get(text).get("path"), str(Path.cwd() / text))
            QMessageBox.warning(self, "Сообщение", "Загрузка завершена в текущую директорию")

    def connect_cloud(self, path: str):
        """
        Получение словаря с данными о папках и файлах в выбранной директории облака.
        """
        if self.files:
            self.ui.pathEdit.setText(self.files.get(path).get("path"))
            files = connect_cloud_folder(self.client, self.files.get(path).get("path"))
            path = "/".join(self.files.get(path).get("path").split("/")[:-1])
            if path:
                self.table_construct(files, path)
            else:
                self.table_construct(files, "/")
        else:
            self.ui.pathEdit.setText(path)
            files = connect_cloud_folder(self.client, path)
            self.table_construct(files, path)

    def table_construct(self, files: list, path: str):
        """
        Заполнение таблицы данными полученными в запросе к облаку.
        """
        self.ui.tableWidget.setRowCount(0)
        row_position = self.ui.tableWidget.rowCount()
        self.ui.tableWidget.insertRow(row_position)
        self.ui.tableWidget.setItem(row_position, 0, QtWidgets.QTableWidgetItem("..."))
        self.files.update({"...": {"path": path, "isdir": True}})
        for path in files:
            row_position = self.ui.tableWidget.rowCount()
            self.ui.tableWidget.insertRow(row_position)
            self.ui.tableWidget.setItem(row_position, 0, QtWidgets.QTableWidgetItem(path.get("name")))
            self.files.update({path.get("name"): {"path": path.get("path"), "isdir": path.get("isdir")}})
        self.ui.tableWidget.resizeColumnsToContents()
        self.ui.tableWidget.setEditTriggers(QtWidgets.QTableWidget.EditTrigger.NoEditTriggers)

    @staticmethod
    def getOpenFilesAndDirs(parent=None, caption='', directory='', filter='', initialFilter='', options=None) -> list:
        """
        Создание окна для выбора файлов или папок для загрузки.
        """
        def updateText():
            selected = []
            for index in view.selectionModel().selectedRows():
                selected.append('"{}"'.format(index.data()))
            lineEdit.setText(' '.join(selected))

        dialog = QtWidgets.QFileDialog(parent, windowTitle=caption)
        dialog.setFileMode(dialog.ExistingFiles)
        if options:
            dialog.setOptions(options)
        dialog.setOption(dialog.DontUseNativeDialog, True)
        if directory:
            dialog.setDirectory(directory)
        if filter:
            dialog.setNameFilter(filter)
            if initialFilter:
                dialog.selectNameFilter(initialFilter)

        dialog.accept = lambda: QtWidgets.QDialog.accept(dialog)

        stackedWidget = dialog.findChild(QtWidgets.QStackedWidget)
        view = stackedWidget.findChild(QtWidgets.QListView)
        view.selectionModel().selectionChanged.connect(updateText)

        lineEdit = dialog.findChild(QtWidgets.QLineEdit)
        dialog.directoryEntered.connect(lambda: lineEdit.setText(''))

        dialog.exec_()
        return dialog.selectedFiles()

    def input_pass(self):
        """
        Диалоговое окно для ввода пароля для шифрования логина и пароля для облака.
        """
        text, ok = QInputDialog.getText(self, 'Ввод пароля', 'Введите пароль:')
        return text if ok else False


class InputDialog(QDialog):
    """
    Создание диалога с двумя полями ввода для логина и пароля облака.
    """
    def __init__(self, parent=None):
        super().__init__(parent)

        self.first = QLineEdit(self)
        self.second = QLineEdit(self)
        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)

        layout = QFormLayout(self)
        layout.addRow("Cloud Mail Login", self.first)
        layout.addRow("Cloud Mail Password", self.second)
        layout.addWidget(button_box)

        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)

    def get_inputs(self) -> tuple:
        return self.first.text(), self.second.text()


def cpu() -> (str, bool):
    """
    Получение серийного номера процессора для шифрования, который
    будет использоваться в Windows вместо пароля для шифрования.
    """
    from wmi import WMI
    cp = WMI().Win32_Processor()[0].ProcessorId
    return cp if cp else False


def encrypt_word(word: str, pw: str) -> str:
    """
    Шифрование логина и пароля.
    """
    return encrypt(word, pw)


if __name__ == '__main__':
    """
    Проверка наличия файла с настройками. Если нет, запрашиваем
    логин и пароль, шифруем и сохраняем в файл.
    Проверяем, если ОС Windows используем для шифрования серийный
    номер процессора. Если получить не удалось, запрашиваем у пользователя
    пароль для шифрования введенных данных для входа в облако.
    Если ОС Linux, запрашиваем у пользователя пароль для шифрования данных
    для входа в облако.
    Авторизация в облаке с полученным серийным номером или введенным паролем.
    Запуск приложения.
    """
    app = QtWidgets.QApplication(argv)
    myapp = MyWin()
    psw = None
    if psys() == "Windows":
        psw = cpu()
        if not psw:
            psw = myapp.input_pass()
            if not psw:
                ext(0)
    elif psys() == "Linux":
        psw = myapp.input_pass()
        if not psw:
            ext(0)
    if not check_setting():
        dialog = InputDialog()
        if dialog.exec():
            login, password = dialog.get_inputs()
            if not login or not password:
                print("Данные для входа в Облако не получены")
                sys.exit(0)
            with open("setting.ini", "w", encoding="utf-8") as file:
                file.write(f'{encrypt_word(login, psw)}\n')
                file.write(f'{encrypt_word(password, psw)}\n')
        else:
            ext(0)
    client = authorize(psw)
    if client:
        myapp.client = client
        myapp.connect_cloud(path="/")
        myapp.show()
        ext(app.exec_())
    else:
        print("Авторизация на удалась. Проверьте файл setting.ini")


А на этом, пожалуй, все.

Спасибо за внимание. Надеюсь, данная информация будет вам полезна
 

Вложения

  • mail_cloud_webdev.zip
    204,9 КБ · Просмотры: 166
Последнее редактирование модератором:
  • Нравится
Реакции: InternetMC
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!