畢設程式碼

20201319吴向林發表於2024-05-26

client.py

import requests
import json
import sys
import os
import hashlib
import threading
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QWidget, QApplication, QMessageBox, QFileDialog, QProgressDialog, QInputDialog, QLineEdit, \
    QPushButton
from PyQt5.QtGui import *
from Ui_LoginClient import Ui_Form
from Ui_MainClient import Ui_Form as mainForm
from QCandyUi.CandyWindow import colorful
import time
from datetime import datetime
import smtplib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from email.mime.text import MIMEText
import pickle
from email.utils import formataddr
import random
import secrets
import stat

SERVER_HOST = 'http://82.156.24.158:80'#'http://192.168.1.120:8000'
EMAIL_PASSCODE = 'cgoizqmcgbhndcdh'
EMAIL_HOST = '2761407387@qq.com'
global userdata
userdata = {}


# @colorful('blueDeep')
class MainGUI(QWidget, mainForm):
    def __init__(
            self) -> None:  # parent: typing.Optional['QWidget'] = ..., flags: typing.Union[QtCore.Qt.WindowFlags, QtCore.Qt.WindowType] = ...
        super().__init__()
        self.setupUi(self)
        # 禁止拉伸視窗
        self.setFixedSize(self.width(), self.height())
        self.setWindowTitle('MainClient')
        # 禁用最大化
        #self.setWindowFlags(Qt.WindowMaximizeButtonHint)
        # 上傳檔案按鈕繫結函式select files
        self.upload.clicked.connect(self.selectFiles)
        # 設定背景圖片,背景圖片顯示,承載控制元件為一個QLabel
        self.background.setPixmap(QPixmap('MainData/background.png'))
        # 設定群組控制元件的標題為使用者ID
        self.groupBox.setTitle(userdata['uid'])
        # 設定檔案展示列表的控制元件背景為透明
        self.filelist.setStyleSheet("background-color: transparent;")
        # 將刪除按鈕繫結函式delete file
        self.delete.clicked.connect(self.deleteFile)
        # 將重新命名按鈕繫結函式renamefile
        self.rename.clicked.connect(self.renameFile)
        self.filelist.itemClicked.connect(self.itemclicker)
        self.downloadmod.clicked.connect(self.downloadFile)
        self.cancelbutton = QPushButton('登出', self)
        self.cancelbutton.setGeometry(QRect(0, 735, 410, 20))
        self.cancelbutton.setObjectName("cancel")
        self.cancelbutton.clicked.connect(self.cancelfunc)
        # 設定預設檔案控制模式為下載
        self.Filecontroler_mode = 'download'  # 另有rename delete模式
        # 初始化重新整理執行緒暫停為否
        self.refresh_pause = False
        self.listviews()
        # 建立執行緒使得檔案列表無阻塞的展示檢視得以自動化重新整理,縣城指定的應用函式為list view
        '''self.filelist_refresher = threading.Thread(target=self.listview)
        self.filelist_refresher.start()'''

        # 同上,現場繫結的函式為refresh net disk
        self.netdisk_refresher = threading.Thread(target=self.refreshNetdisk)
        self.netdisk_refresher.start()

    def refreshNetdisk(self):
        # 此函式用於迴圈請求來重新整理剩餘的磁碟空間
        while True:
            if not self.refresh_pause:
                # 當執行緒暫停未使用時,則開始請求伺服器獲得一份新的磁碟,剩餘空間資料
                #diskstatus_start_time =time.time()
                diskstatus = json.loads(requests.get(SERVER_HOST + '/diskstatus').content)
                # 將剩餘的磁碟空間展示在groupbox裡面的QLabel控制元件中
                #diskstatus_end_time =time.time()
                #diskstatus_time =diskstatus_end_time - diskstatus_start_time 
                #print(f'diskstatus_time:{diskstatus_time} seconds')
                self.diskchecker.setText(
                    f"<html><head/><body><p><span style=\" font-size:20pt; font-weight:600; color:#55ff7f;\">剩餘可用空間(MB):{diskstatus['data']}</span></p></body></html>")
                # 休眠三秒減少伺服器負擔
                time.sleep(2)
            else:
                # 當執行緒暫停被使用時,暫停三秒,減少迴圈pass帶來的本地客戶端資源佔用
                time.sleep(2)
                pass

    def deleteFile(self):
        # 切換點選項的模式為刪除模式
        QMessageBox.about(self, u'提示', "已切換為刪除模式\n非重新命名模式將自動恢復/啟動檔案與磁碟重新整理")
        self.Filecontroler_mode = 'delete'
        self.refresh_pause = False

    def downloadFile(self):
        # 切換檔案控制模式為下載
        QMessageBox.about(self, u'提示', "已切換為下載模式\n非重新命名模式將自動恢復/啟動檔案與磁碟重新整理")
        self.refresh_pause = False
        self.Filecontroler_mode = 'download'

    def renameFile(self):
        # 切換點選項的模式為重新命名模式
        QMessageBox.about(self, u'提示', "已切換為重新命名模式\n自動化檔案重新整理與磁碟重新整理暫停")
        self.refresh_pause = True
        # 此時,需要將執行緒暫停的開關設定為True
        # 當請求獲取檔案列表的時候,服務端會佔用指定檔案的許可權,則此時需要暫停檔案的重新整理來釋放佔用,才能請求重新命名的介面,使之自由操作檔案
        self.Filecontroler_mode = 'rename'

    def itemclicker(self, item):
        # 此處為filelist的每一項點選時會觸發的函式,傳入item
        if self.Filecontroler_mode == 'download':
            # 當檔案控制模式為下載時,詢問是否建立點選檔案的下載執行緒
            pre_to_downloadFile = QMessageBox.question(self, 'pre_to_downloadFile',
                                                       f'是否建立檔案:{item.text()}    的下載執行緒',
                                                       QMessageBox.Yes | QMessageBox.No)
            # 當使用者點選確認時,則獲取點選項的文字,請求伺服器進行下載
            if pre_to_downloadFile == QMessageBox.Yes:
                self.log.addItem(f'{datetime.today()}:開始下載{item.text()} (執行緒已建立)')
                postdata_log = {
                    'uid': userdata['uid'],
                    'log': f'{datetime.today()}:開始下載{item.text()} (執行緒已建立)'
                }
                postdata_log = json.dumps(postdata_log)
                # 上傳檔案與表單到upload files介面
                logging_start_time =time.time()
                 
                #print(f'logging_time:{logging_time} seconds')
                requests.post(SERVER_HOST + '/logging', data=postdata_log)
                # 新建執行緒傳參,防止在下載大檔案時,主程式未響應
                logging_end_time =time.time()
                logging_time =logging_end_time - logging_start_time
                print(f'logging_time:{logging_time} seconds')
                download_thread = threading.Thread(target=self.downloader, args=(item.text(),))
                download_thread.start()
            else:
                # 反之使用者未點選確認時則pass
                pass
        elif self.Filecontroler_mode == 'delete':
            # 同上
            pre_to_deleteFile = QMessageBox.question(self, 'pre_to_deleteFile',
                                                     f'是否建立檔案:{item.text()}    的刪除執行緒',
                                                     QMessageBox.Yes | QMessageBox.No)
            if pre_to_deleteFile == QMessageBox.Yes:
                self.log.addItem(f'{datetime.today()}:開始刪除{item.text()} (執行緒已建立)')
                postdata_log = {
                    'uid': userdata['uid'],
                    'log': f'{datetime.today()}:開始刪除{item.text()} (執行緒已建立)'
                }
                postdata_log = json.dumps(postdata_log)
                requests.post(SERVER_HOST + '/logging', data=postdata_log)
                delete_thread = threading.Thread(target=self.deleter, args=(item.text(),))
                delete_thread.start()
            else:
                pass
        elif self.Filecontroler_mode == 'rename':
            # 當檔案控制模式為重新命名時,則彈出一個輸入彈窗
            value, ok = QInputDialog.getText(self, "renameInput",
                                             f"請在下方輸入要將 {item.text()} 重新命名的內容(包括字尾)", QLineEdit.Normal,
                                             "")
            # 當使用者輸入並點選ok時則獲取輸入文字
            if not ok:
                self.log.addItem(f'{datetime.today()}:取消了重新命名{item.text()}')
                postdata_log = {
                    'uid': userdata['uid'],
                    'log': f'{datetime.today()}:取消了重新命名{item.text()}'
                }
                postdata_log = json.dumps(postdata_log)
                requests.post(SERVER_HOST + '/logging', data=postdata_log)
            else:
                self.log.addItem(f'{datetime.today()}:開始重新命名{item.text()} 為 {value}(執行緒已建立)')
                postdata_log = {
                    'uid': userdata['uid'],
                    'log': f'{datetime.today()}:開始重新命名{item.text()} 為 {value}(執行緒已建立)'
                }
                postdata_log = json.dumps(postdata_log)
                requests.post(SERVER_HOST + '/logging', data=postdata_log)
                print(value)
                # 建立執行緒進行重新命名
                self.renamer(item.text(), value)

    def deleter(self, filename):
        # 當刪除函式被觸發時
        postdata = {
            'uid': userdata['uid'],
            'token': userdata['token'],
            'filename': filename
        }
        # 將身份驗證作為請求體,傳入刪除介面
        #deleteFiles_end_time =time.time()
        #deleteFiles_time =deleteFiles_end_time - deleteFiles_start_time 
        #print(f'deleteFiles_time:{deleteFiles_time} seconds')
        deleteFiles_start_time =time.time()
        respond = json.loads(requests.post(f'{SERVER_HOST}/deleteFiles', data=json.dumps(postdata)).content)
        deleteFiles_end_time =time.time()
        deleteFiles_time =deleteFiles_end_time - deleteFiles_start_time
        print(f'deleteFiles_time:{deleteFiles_time} seconds')
        try:
            # 當返回碼為-5時表示請求錯誤,並列印請求日誌
            if respond['code'] == -5:
                self.log.addItem(respond['message'])
                postdata = {
                    'uid': userdata['uid'],
                    'log': respond['message']
                }
                requests.post(SERVER_HOST + '/logging', data=postdata)
                self.listviews()
                return 0
        except:
            self.listviews()
            return 0
        os.remove(f"ClientData/{filename}_aesKey.pickle")
        self.log.addItem(f'{datetime.today()}:刪除{filename}成功!')
        postdata_log = {
            'uid': userdata['uid'],
            'log': f'{datetime.today()}:刪除{filename}成功!'
        }
        postdata_log = json.dumps(postdata_log)
        requests.post(SERVER_HOST + '/logging', data=postdata_log)
        self.listviews()
        return 0

    def renamer(self, filename, updatename):
        # 當此重新命名函式被觸發時,獲取傳入的舊檔名與填入的更新後檔名
        postdata = {
            'uid': userdata['uid'],
            'token': userdata['token'],
            'filename': filename,
            'update': updatename
        }
        #renameFiles_end_time =time.time()
        #renameFiles_time =renameFiles_end_time - renameFiles_start_time 
        #print(f'renameFiles_time:{renameFiles_time} seconds')
        renameFiles_start_time =time.time()
        respond = json.loads(requests.post(f'{SERVER_HOST}/renameFiles', data=json.dumps(postdata)).content)
        renameFiles_end_time =time.time()
        renameFiles_time =renameFiles_end_time - renameFiles_start_time 
        print(f'renameFiles_time:{renameFiles_time} seconds')
        try:
            if respond['code'] == -5:
                self.log.addItem(respond['message'] + '\n\n請重試')
                postdata_log = {
                    'uid': userdata['uid'],
                    'log': respond['message'] + '\n\n請重試'
                }
                postdata_log = json.dumps(postdata_log)
                requests.post(SERVER_HOST + '/logging', data=postdata_log)
                return 0
        except:
            if os.path.exists(f'ClientData\\{filename}_aesKey.pickle'):
                with open(f'ClientData\\{filename}_aesKey.pickle', 'rb') as o:
                    orifile = pickle.load(o)
                o.close()
                os.rename(f'ClientData\\{filename}_aesKey.pickle', f'ClientData\\{updatename}_aesKey.pickle')
                with open(f'ClientData\\{updatename}_aesKey.pickle', 'wb') as r:
                    pickle.dump({
                        updatename: orifile[filename]
                    }, r)
                    r.close()
            else:
                pass
            self.log.addItem(f'{datetime.today()}:重新命名{filename}為{updatename}成功!')
            postdata_log = {
                'uid': userdata['uid'],
                'log': f'{datetime.today()}:重新命名{filename}為{updatename}成功!'
            }
            postdata_log = json.dumps(postdata_log)
            self.listviews()
            requests.post(SERVER_HOST + '/logging', data=postdata_log)
            self.tmp_refresh()
        return 0

    def cancelfunc(self):
        select = QMessageBox.question(self, 'Cancel your account?',
                                      f'當前點選了登出賬號,您確定需要登出嗎,這將會清除您的雲端儲存資料',
                                      QMessageBox.Yes | QMessageBox.No)
        if select == QMessageBox.Yes:
            value, ok = QInputDialog.getText(self, 'CANCEL?', f"請輸入使用者:{userdata['uid']}的密碼完成二次校驗")
            if not ok:
                self.log.addItem(f'{datetime.today()}:取消了登出操作')
                postdata_log = {
                    'uid': userdata['uid'],
                    'log': f'{datetime.today()}:取消了登出操作'
                }
                postdata_log = json.dumps(postdata_log)
                requests.post(SERVER_HOST + '/logging', data=postdata_log)
            else:
                inputhash = hashlib.sha256((str(userdata['uid']) + str(value)).encode('utf-8')).hexdigest()
                if inputhash == userdata['hash']:
                    postdata = {
                        'uid': userdata['uid']
                    }
                    CancelAccount_start_time =time.time()
                    CancelAccount_end_time =time.time()
                    
                    
                    res = requests.post(f'{SERVER_HOST}/CancelAccount', data=json.dumps(postdata)).content
                    CancelAccount_end_time =time.time()
                    CancelAccount_time = CancelAccount_end_time - CancelAccount_start_time
                    print(f'CancelAccount_time:{CancelAccount_time} seconds')
                    self.log.addItem(f'{datetime.today()}:登出成功:{res}')
                    postdata_log = {
                        'uid': userdata['uid'],
                        'log': f'{datetime.today()}:登出操作'
                    }
                    postdata_log = json.dumps(postdata_log)
                    requests.post(SERVER_HOST + '/logging', data=postdata_log)
                    self.refresh_pause = True
                    self.close()
        else:
            self.log.addItem(f'{datetime.today()}:取消了登出操作')
            postdata_log = {
                'uid': userdata['uid'],
                'log': f'{datetime.today()}:取消了登出操作'
            }
            postdata_log = json.dumps(postdata_log)
            requests.post(SERVER_HOST + '/logging', data=postdata_log)

    def selectFiles(self):
        # 當點選上傳檔案時觸發select file函式
        self.directory, status = QFileDialog.getOpenFileNames(self, '請選擇需要上傳的檔案 允許多選', "C:\\")
        print(self.directory)
        # 返回為一個列表,當列表長度為零時,表示未選擇任何檔案,則直接結束選擇函式
        if len(self.directory) == 0:
            return None
        # 當選擇長度不為0時,則提出二次驗證是否上傳
        pre_to_uploadFile = QMessageBox.question(self, 'FileSelectSuccess',
                                                 f'當前選中了\n{self.directory}\n是否確認上傳',
                                                 QMessageBox.Yes | QMessageBox.No)
        is_clientEncrypt = QMessageBox.question(self, 'ClientEncrypt',
                                                f'是否在本地完成加密?伺服器端也會進行一次加密,如果您不希望伺服器有哪怕一瞬間獲取您的檔案,您可以選擇上傳伺服器前確認此對話方塊來在本地離線完成加密再上傳',
                                                QMessageBox.Yes | QMessageBox.No)
        if is_clientEncrypt == QMessageBox.Yes:
            self.clientEncrypt = True
        else:
            self.clientEncrypt = False
        self.count = 0
        if pre_to_uploadFile == QMessageBox.Yes:
            # 分別建立執行緒,一個檔案對應一個執行緒,提交每一個檔案的路徑給執行緒作為傳參
            for task in range(len(self.directory)):
                tasker = threading.Thread(target=self.acceptToUploadFile, args=(self.count,))
                tasker.start()
                self.count += 1
        else:
            pass

    def get_user_aeskey_oridata(filename: str) -> dict:
        # 獲取aes金鑰檔案的原始資料字典
        try:
            with open(f"ClientData/{filename}_aesKey.pickle", 'rb') as r:
                userdata = pickle.load(r)
                r.close()
            return userdata
        except:
            return {}

    def acceptToUploadFile(self, count):
        # 此為上傳函式用於接收select file建立的執行緒
        task_filepath = self.directory[count]
        if self.clientEncrypt == True:
            key = bytes(secrets.token_hex(16), 'utf-8')
            # ecb模式生成金鑰加密
            aes_key_ecb = AES.new(key, AES.MODE_ECB)
            # 呼叫方法便捷儲存使用者aes金鑰檔案
            filename = os.path.basename(task_filepath)
            with open(f'ClientData/{filename}_aesKey.pickle', 'wb') as saveKey:
                oridata = MainGUI.get_user_aeskey_oridata(filename=filename)
                oridata[filename] = key
                pickle.dump(oridata, saveKey)
                print(filename, '儲存aeskey')
                saveKey.close()
            # 獲取使用者加密後的bytes,encrypt函式中需要pad來補全aes需要的位寬,如不填滿位寬則無法加密
            with open(task_filepath, 'rb') as tmp:
                tmpfile = tmp.read()
                encode_return = aes_key_ecb.encrypt(pad(tmpfile, AES.block_size))
                with open(f'ClientData/{filename}', 'wb') as tmpwrite:
                    tmpwrite.write(encode_return)
                    tmpwrite.close()
                tmp.close()
            task_filepath = f'ClientData/{filename}'
        # 獲取列表中指定的執行緒
        # Upload qp回一個q progress dialog用於實現進度條
        uploadQP = QProgressDialog(self)
        uploadQP.setWindowTitle(f'{task_filepath}')
        uploadQP.setWindowFlag(Qt.WindowCloseButtonHint, False)
        file = open(task_filepath, 'rb')
        # filedata用於上傳檔案的bytes
        filedata = {
            'file': file
        }
    
        # Request header用於構建使用者個人資訊作為表單上傳
        request_header = {
            'token': userdata['token'],
            'uid': userdata['uid']
        }
        postdata = {
            'data': json.dumps(request_header)
        }
        # 上傳檔案與表單到upload files介面
        #uploadFiles_end_time =time.time()
        #uploadFiles_time =uploadFiles_end_time - uploadFiles_start_time 
        #print(f'uploadFiles_time:{uploadFiles_time} seconds')
        uploadFiles_start_time =time.time()
        uploader = requests.post(SERVER_HOST + '/uploadFiles', files=filedata, data=postdata).content
        uploadFiles_end_time =time.time()
        uploadFiles_time =uploadFiles_end_time - uploadFiles_start_time 
        print(f'uploadFiles_time:{uploadFiles_time} seconds')
        print(uploader)
        self.log.addItem(json.loads(uploader)['status'])
        postdata_log = {
            'uid': userdata['uid'],
            'log': json.loads(uploader)['status']
        }
        postdata_log = json.dumps(postdata_log)
        requests.post(SERVER_HOST + '/logging', data=postdata_log)
        file.close()
        self.listviews()
        # os.remove(f'ClientData/{filename}')

    def downloader(self, item):
        # 此函式用於接收下載建立的執行緒
        if not os.path.exists('ALE_Netdisk_download/'):
            os.makedirs('ALE_Netdisk_download/')
        if not os.path.exists(f"ClientData/{item}_aesKey.pickle"):
            ClientEncrypt = False
        else:
            ClientEncrypt = True
        # 構建個人資訊請求體
        postdata = {
            'uid': userdata['uid'],
            'filename': item,
            'token': userdata['token'],
        }
        #downloadFiles_end_time =time.time()
        #downloadFiles_time =downloadFiles_end_time - downloadFiles_start_time 
        #print(f'downloadFiles_time:{downloadFiles_time} seconds')
        downloadFiles_start_time =time.time()
        respond = requests.post(f'{SERVER_HOST}/downloadFiles', data=json.dumps(postdata))
        downloadFiles_end_time =time.time()
        downloadFiles_time =downloadFiles_end_time - downloadFiles_start_time 
        print(f'downloadFiles_time:{downloadFiles_time} seconds')
        # 服務端返回已解密的檔案,直接按照item.text()返回的檔名寫入即可
        downloadFiles_end_time =time.time()
        downloadFiles_time =downloadFiles_end_time - downloadFiles_start_time 
        print(f'downloadFiles_time:{downloadFiles_time} seconds')
        savepath = f'ALE_Netdisk_download/{item}'
        filedata = respond.content
        if ClientEncrypt:
            self.log.addItem(f'{datetime.today()}:檢查到{item}為本地二次加密檔案,執行離線解密中')
            with open(f"ClientData/{item}_aesKey.pickle", 'rb') as key:
                savekey = pickle.load(key)[item]
                print(savekey)
                key.close()
            aes_key = AES.new(savekey, AES.MODE_ECB)
            # 解密檔案
            defile = aes_key.decrypt(filedata)
            # 對加密時使用pad填充的位寬進行消除,使用unpad方法
            filedata = unpad(defile, AES.block_size)
        print(savepath)
        try:
            with open(savepath, 'wb') as sv:
                sv.write(filedata)
                sv.close()
        except:
            pass
        self.log.addItem(f'{datetime.today()}:下載{item}成功!')
        # 當寫入成功後,則請求移除臨時檔案介面
        requests.post(f'{SERVER_HOST}/rmFiles', data=json.dumps(postdata))
        postdata_log = {'uid': userdata['uid'], 'log': f"{datetime.today()}:下載{item}成功!"}
        postdata_log = json.dumps(postdata_log)
        requests.post(SERVER_HOST + '/logging', data=postdata_log)

    def tmp_refresh(self):
        # 此介面用於rename函式在完成重新命名之後,尚未切換到其他模式來解除refresh pause導致的未能及時重新整理檔案列表,因而,構建此函式用於臨時重新整理單次檔案列表
        font = QFont()
        font.setPointSize(20)
        self.filelist.setFont(font)
        reqdata = {
            'uid': userdata['uid'],
            'token': userdata['token']
        }
        self.filelist_ori = json.loads(requests.post(SERVER_HOST + '/filelist', data=json.dumps(reqdata)).content)[
            'data']
        self.filelist.clear()
        for fl in self.filelist_ori:
            self.filelist.addItem(fl)

    def listviews(self):
        # 此函式用於接收執行緒來構建自動化,定時重新整理檔案列表
        font = QFont()
        # 例項化字型並且應用於List widget
        font.setPointSize(20)  # 字型大小可修改為任意數
        self.filelist.setFont(font)
        if not self.refresh_pause:
            reqdata = {
                'uid': userdata['uid'],
                'token': userdata['token']
            }
            self.filelist_ori = \
            json.loads(requests.post(SERVER_HOST + '/filelist', data=json.dumps(reqdata)).content)['data']
            #filelist_start_time =time.time()
            #filelist_end_time =time.time()
            #filelist_time =filelist_end_time - filelist_start_time 
            #print(f'filelist_time:{filelist_time} seconds')
            self.filelist.clear()
            for fl in self.filelist_ori:
                self.filelist.addItem(fl)
                
    def listview(self):
        # 此函式用於接收執行緒來構建自動化,定時重新整理檔案列表
        font = QFont()
        # 例項化字型並且應用於List widget
        font.setPointSize(20)  # 字型大小可修改為任意數
        self.filelist.setFont(font)
        while True:
            # 死迴圈在請求介面之前判斷是否已經啟用執行緒重新整理暫停
            if not self.refresh_pause:
                reqdata = {
                    'uid': userdata['uid'],
                    'token': userdata['token']
                }
                self.filelist_ori = \
                json.loads(requests.post(SERVER_HOST + '/filelist', data=json.dumps(reqdata)).content)['data']
                self.filelist.clear()
                for fl in self.filelist_ori:
                    self.filelist.addItem(fl)
                time.sleep(2)
            else:
                time.sleep(2)
                pass


# @colorful('blueDeep') 禁用裝飾器原因:close()方法無法完全關閉裝飾後的ui
class LoginGUI(QWidget, Ui_Form):
    def __init__(
            self) -> None:  # parent: typing.Optional['QWidget'] = ..., flags: typing.Union[QtCore.Qt.WindowFlags, QtCore.Qt.WindowType] = ...
        super().__init__()
        self.setupUi(self)
        # 禁止拉伸視窗
        self.setFixedSize(self.width(), self.height())
        self.setWindowTitle('LoginClient')
        #self.setWindowFlags(Qt.WindowMaximizeButtonHint)
        self.backgroud.setPixmap(QPixmap('MainData/background.png'))
        self.loginbutton.clicked.connect(self.login)
        self.register.clicked.connect(self.registerfunc)

    def login(self):
        # 獲取兩個登入視窗的輸入框內容
        uid = self.uidinput.text()
        password = self.passwordinput.text()
        hashcode = hashlib.sha256(bytes(f"{uid}{password}", 'utf-8')).hexdigest()
        postdata = {
            'uid': uid,
            'hash': hashcode
        }
        #login_end_time =time.time()
        #login_time =login_end_time - login_start_time 
        #print(f'login_time:{login_time} seconds')
        login_start_time =time.time()
        respond = requests.post(f'{SERVER_HOST}/login', data=json.dumps(postdata)).content
        login_end_time =time.time()
        login_time =login_end_time - login_start_time 
        print(f'login_time:{login_time} seconds')
        respond = json.loads(respond)
        if respond['code'] != 1:
            # 當返回的狀態碼不為1時則表示請求錯誤,並且以警告框提示出錯誤的描述文字
            QMessageBox.critical(self, 'LoginError', f"{respond['message']}", QMessageBox.Ok)
            return False
        else:
            # 登入成功後,在客戶端執行的主體中,建立一個公有的字典,並且將使用者資料寫入到這個字典中,字典的生命週期結束於使用者關閉客戶端程式視窗
            QMessageBox.about(self, 'LoginSuccess', f"登入成功!歡迎您:{uid}")
            self.token = respond['token']
            userdata['token'] = self.token
            userdata['uid'] = uid
            userdata['hash'] = hashcode
            # self.setuserbox
            # Close方法關閉當前視窗,並且清除當前的控制元件
            self.close()
            # 啟用主視窗
            GUIcontroller.runMain()

    def registerfunc(self):
        # 此函式用於註冊
        uid = self.uidinput.text()
        password = self.passwordinput.text()
        if uid == '':
            QMessageBox.critical(self, 'registerError', f"賬號未輸入", QMessageBox.Ok)
            return False
        if password == '':
            QMessageBox.critical(self, 'registerError', f"密碼未輸入", QMessageBox.Ok)
            return False
        hashcode = hashlib.sha256(bytes(f"{uid}{password}", 'utf-8')).hexdigest()
        postdata = {
            'uid': uid,
            'hash': hashcode
        }
        # 此處當使用者點選註冊,並且賬號密碼都已輸入的同時,則彈出輸入框,要求輸入郵箱地址用於驗證
        value, ok = QInputDialog.getText(self, "reg_input_emailAddress", f"請輸入您的郵箱地址", QLineEdit.Normal, "")
        if not ok:
            # 如果選擇了取消返回空
            return None
        else:
            # 如果選擇了OK,浙江輸入的郵箱地址傳入到郵箱驗證介面中來,驗證此郵箱是否已被註冊
            #regchecker_end_time =time.time()
            #regchecker_time =regchecker_end_time - regchecker_start_time 
            #print(f'regchecker_time:{regchecker_time} seconds')
            regchecker_start_time =time.time()
            email_isreg_cheaker = requests.post(f'{SERVER_HOST}/regchecker', data=json.dumps({'email': value})).text
            regchecker_end_time =time.time()
            regchecker_time =regchecker_end_time - regchecker_start_time 
            print(f'regchecker_time:{regchecker_time} seconds')
            print(email_isreg_cheaker)
            # 如果返回為false時則表示郵箱已被註冊
            if email_isreg_cheaker == 'false':
                QMessageBox.critical(self, 'registerError', f"此郵箱已被註冊", QMessageBox.Ok)
                return False
            # 隨機生成五位驗證碼
            random_code = random.randint(10000, 99999)
            sender = user = EMAIL_HOST  # 傳送方的郵箱賬號
            passwd = EMAIL_PASSCODE  # 授權碼
            receiver = value  # 接收方的郵箱賬號,不一定是QQ郵箱
            # 純文字內容 
            msg = MIMEText(f'您的驗證碼為{random_code}', 'plain', 'utf-8')
            # From 的內容是有要求的,前面的abc為自己定義的 nickname,如果是ASCII格式,則可以直接寫
            msg['From'] = user
            msg['To'] = receiver
            msg['Subject'] = f'ALE netdisk 驗證'  # 點開詳情後的標題
            try:
                # 建立 SMTP 、SSL 的連線,連線傳送方的郵箱伺服器
                smtp = smtplib.SMTP_SSL('smtp.qq.com', 465)
                # 登入傳送方的郵箱賬號
                smtp.login(user, passwd)
                # 傳送郵件 傳送方,接收方,傳送的內容
                smtp.sendmail(sender, receiver, msg.as_string())
                print('郵件傳送成功')
                smtp.quit()
                sms_checker, smsok = QInputDialog.getText(self, "reg_input_code",
                                                          f"驗證碼已傳送至{value}\n請在下方輸入", QLineEdit.Normal, "")
                if not smsok:
                    return None
                # 校驗驗證碼
                elif sms_checker != str(random_code):
                    QMessageBox.critical(self, 'registerError', f"驗證碼錯誤", QMessageBox.Ok)
                    return False
                else:
                    # 當驗證碼正確時,在請求體中寫入郵箱
                    # 郵箱可用性驗證在本地進行,同時,為了保證安全性,可以部署在伺服器端,由於只需要校驗郵箱是否為可用郵箱,所以部署在本地即可
                    postdata['email'] = value
            except Exception as e:
                # 出現不可預料的錯誤,諸如網路因素或者郵箱以及提供的stmp碼不正確時列印錯誤資訊
                QMessageBox.critical(self, 'registerError', f"{e}", QMessageBox.Ok)
                return False
            # 將最新構建的post data上傳至伺服器進行註冊登記
            #register_end_time =time.time()
            #register_time =register_end_time - register_start_time 
            #print(f'register_time:{register_time} seconds')
            register_start_time =time.time()
            respond = requests.post(f'{SERVER_HOST}/register', data=json.dumps(postdata)).content
            register_end_time =time.time()
            register_time =register_end_time - register_start_time 
            print(f'register_time:{register_time} seconds')
            respond = json.loads(respond)
            if respond['code'] != 1:
                # 當註冊返回不為一時表示註冊發生了失敗,但是請求正常,並非網路因素,則列印錯誤資訊,此處一般情況下錯誤僅提示使用者已註冊
                QMessageBox.critical(self, 'registerError', f"{respond['message']}", QMessageBox.Ok)
                return False
            else:
                # 註冊成功後,把獲取的個人資訊以及token寫入到公共的字典中,此字典的生命週期結束於客戶端關閉
                QMessageBox.about(self, 'registerSuccess', f"註冊成功!歡迎您:{uid}")
                self.token = respond['token']
                userdata['token'] = self.token
                userdata['uid'] = uid
                userdata['hash'] = hashcode
                self.close()
                GUIcontroller.runMain()


class GUIcontroller(object):
    # 此類用於控制視窗的切換,防止視窗與視窗之間建立執行緒或切換時,會引發異常,因此構建一個控制體
    def __init__(self) -> None:
        pass

    def login(self):
        self.login_main = LoginGUI()
        self.login_main.show()

    def runMain():
        main_gui = MainGUI()
        main_gui.show()


if __name__ == "__main__":
    if not os.path.exists('ClientData/'):
        os.makedirs('ClientData/')
        print('creater')
    app = QApplication(sys.argv)
    controller = GUIcontroller()
    # 初始化控制元件並且啟用登入頁
    controller.login()
    sys.exit(app.exec())

server.py

import os
import hashlib
from typing import List
from fastapi import FastAPI,Body,UploadFile,File,Request,Form
from starlette.responses import FileResponse
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
from Crypto import Random
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
import base64
import threading
import rsa
import time
import logging
import uvicorn
import pickle
import json
from pathlib import Path
import socket
from datetime import datetime
import secrets
import os
import platform
import ctypes
import shutil
server = FastAPI()

class webserver(object):

    #構架一個web介面類,分佈開發伺服器api介面
    def __init__(self) -> None:
        uvicorn.run(host=f'{api.getlocalip()}',port=8000,app=server)



    @server.get('/status')
    async def status():
        return {
            'code':200,
            'message':'服務存活'
        }
    
    @server.post('/CancelAccount')
    async def status(reqbody=Body(...)):
        try:
            uid = reqbody['uid']
        except Exception as err:
            return {
                'code':-1,
                'message':f'請求body錯誤\n{err}'
            }
        try:
            shutil.rmtree(f'user/{uid}/')
            return '登出成功,使用者資料已全部清除'
        except Exception as err:
            return {
                    'code':-3,
                    'message':f'此使用者不存在,請註冊-{err}'
                }
    
    @server.post('/logging')
    async def status(reqbody=Body(...)):
        try:
            uid = reqbody['uid']
        except Exception as err:
            return {
                'code':-1,
                'message':f'請求body錯誤\n{err}'
            }
        if not os.path.exists(f'user/{uid}/logging.txt'):
            with open(f'user/{uid}/logging.txt','w') as log:
                log.write('')
                log.close()
        with open(f'user/{uid}/logging.txt','r') as orilog:
            orilogfile = orilog.read()
            orilog.close()
        with open(f'user/{uid}/logging.txt','w') as log:
            newlog = str(orilogfile) + '\n' + reqbody['log']
            log.write(newlog)
            log.close()
            
        
    @server.get('/diskstatus')
    async def status():
        return {
            'code':200,
            #呼叫封裝的介面,獲取磁碟剩餘可用空間
            'data':api.get_free_space_mb()#此處可傳參傳入磁碟機代號,預設c盤
        }

    @server.post('/regchecker')
    async def regie(reqbody=Body(...)):
        #此介面用於檢視郵箱是否被註冊
        #從傳入體中獲取傳入的郵箱地址
        email = reqbody['email']
        try:
            #嘗試去開啟驗證檔案,如果報錯則為檔案不存在,返回Ture的同時寫入一份預設的檔案
            with open('email_isreg.pickle','rb') as r:
                #pickle載入檔案,檔案內為一個序列化的列表
                data = pickle.load(r)
                #關閉釋放檔案佔用
                r.close()
                for t in data:
                    #從已存在的郵箱地址中逐一取值,遍歷每一個進行比較,如果有任何一個與傳入的郵箱一致,那麼判斷已被註冊,返回False
                    if email == t:
                        return False
                    else:
                        pass
                #如果沒有一個與之匹配者返回ture表示郵箱未被註冊
                return True
        except:
            #這裡是當檔案不存在時,寫入一份預設的檔案
            with open('email_isreg.pickle','wb') as w:
                default_data = ['test@qq.com']
                pickle.dump(default_data,w)
                w.close()
            #同時返回ture表達此郵箱未被註冊
            return True

    @server.post('/register')
    async def register(reqbody=Body(...)):
        #如下的程式碼有許多都用到了空檔案判斷,由於各個介面的需求不同,並未封裝成函式,實際需要實踐的思想都一樣,就是當檔案或目錄不存在時,新建一個預設的檔案以及建立目錄,並且寫入相應的檔案
        if not os.path.exists(f"user/{reqbody['uid']}/"):
            os.makedirs(f"user/{reqbody['uid']}/")
            os.makedirs(f"user/{reqbody['uid']}/file/")
            with open(f"user/{reqbody['uid']}/{reqbody['uid']}.pickle",'wb') as sv:
                #user/{username}/{username}.pickle是一份存放了使用者UID,hash code以及他的臨時token還有他的郵箱地址的檔案
                #呼叫封裝好的token構建方法,傳入UID以及hash code來新建一個可用的token,並且傳送到客戶端,允許其進行上傳下載,重新命名等操作,Token的生命週期結束於下一次登入時重新整理
                token = api.token_creator(data={
                        'uid':reqbody['uid'],
                        'hash':reqbody['hash']
                        })
                #這份字典用於儲存使用者資料
                savedata = {
                    'uid':reqbody['uid'],
                    'email':reqbody['email'],
                    'hash':reqbody['hash'],#hashlib.sha256((str(reqbody['uid'])+str(reqbody['password'])).encode('utf-8')).hexdigest(),
                    'token':token
                }
                #pickle dump資料到檔案中
                pickle.dump(savedata,sv)
                #close方法為了解除檔案佔用,方便下一次開啟不出現檔案讀出為空或報錯檔案佔用等問題,後面不再贅述
                sv.close()
            with open('email_isreg.pickle','rb') as r:
                #此處不使用try語句,是由於當emailchecker介面被呼叫時,就已經建立了預設檔案,此處無需判斷檔案是否存在
                #載入一份舊的資料,並且在此基礎上追加,以保證資料完整性
                data = pickle.load(r)
                r.close()
            with open('email_isreg.pickle','wb') as w:
                #在舊資料基礎上追加並且儲存
                data.append(reqbody['email'])
                pickle.dump(data,w)
                w.close()
            return {
                'code':1,
                'message':'註冊成功',
                'token':token
                }
        else:
            #當檔案已存在時表示,此使用者檔案已建立意味著賬號已被建立,則返回資料表明拒絕註冊
            return {
                'code':-4,
                'message':'此使用者已被註冊'
            }
    


    @server.post('/filelist')
    async def filelist(reqbody = Body(...)):
        try:
            with open(f"user/{reqbody['uid']}/{reqbody['uid']}.pickle",'rb') as ck:
                fl = pickle.load(ck)
                ck.close()
            uid = reqbody['uid']
            token = reqbody['token']
            if not token == fl['token']:
                return {
                    'code':-5,
                    'message':'token失效或賬戶不存在'
                }
            else:
                return {
                    'code':1,
                    'message':'請求成功',
                    'data':os.listdir(f"user/{uid}/file")
                }
        except Exception as err:
            return {
                'code':-5,
                'message':'token失效或賬戶不存在\n'+str(err)
            }
    


    @server.post("/uploadFiles")
    #UploadFile好處:支援大檔案,對記憶體壓力小
    async def update_item(file: UploadFile = File(...),data=Form(...)):
        '''#列表推導式排出上傳的檔案列表
        #無論本地端是否實現了多執行緒上傳都支援多檔案同時上傳
        lists = [i.filename for i in files]
        '''
        #這部分由於傳入檔案時Requests會把檔案的位元組以及傳入的字典都封裝到一起,造成沒辦法分離解析,只能透過另開from表單來獲取,因而需要使用json載入字典
        reqbody = json.loads(data)
        uid = reqbody['uid']
        token = reqbody['token']
        #呼叫封裝的介面判斷令牌是否允許使用,如果可用,則返回ture
        if not api.token_acceptuse(uid,token):
            return {
                'code':-1,
                'message':'token已過期或uid不存在'
            }
        
        #使用者的儲存檔案目錄
        with open(f"user/{uid}/file/{file.filename}",'wb') as f:
            #await 非同步等待檔案讀取 type:bytes
            filedata = await file.read()
            #呼叫封裝介面獲取使用者的hashcode
            hashcode = api.get_user_hashcode(uid=uid)
            #args: usetype(encode/decode):str,data[userid,hash,fileid,file:bytes])
            #呼叫aes工具介面選擇加密,並且依次傳入檔案的bytes,以及使用者的標識等資料
            encode_data = api.aestool_func(usetype='encode',data=[uid,hashcode,file.filename,filedata])
            #介面返回加密後的資料,寫入到已開啟的檔案中,儲存於服務端,此時實現了檔案的加密儲存
            f.write(encode_data)
            f.close()
        return {
            'status':f'已成功上傳:{file.filename}'
        }

    @server.post('/rmFiles')
    async def rmFile(reqbody=Body(...)):
        #此介面具備身份校驗,有且僅允許使用者移出臨時檔案
        #當下載結束後允許刪除臨時完整檔案
        with open(f"user/{reqbody['uid']}/{reqbody['uid']}.pickle",'rb') as ck:
            fl = pickle.load(ck)
            ck.close()
        uid = reqbody['uid']
        token = reqbody['token']
        if not token == fl['token']:
            return {
                'code':-5,
                'message':'token失效或賬戶不存在'
            }
        else:
            try:
                os.remove(f"user/{reqbody['uid']}/origin_{reqbody['filename']}")
            except:
                pass
            return "Success"
            
            
    @server.post('/downloadFiles')
    async def download(reqbody=Body(...)):
        #此介面用於提供使用者下載檔案
        with open(f"user/{reqbody['uid']}/{reqbody['uid']}.pickle",'rb') as ck:
            fl = pickle.load(ck)
            ck.close()
        uid = reqbody['uid']
        token = reqbody['token']
        if not token == fl['token']:
            return {
                'code':-5,
                'message':'token失效或賬戶不存在'
            }
        else:
            #token可用後的操作
            with open(f"user/{uid}/file/{reqbody['filename']}",'rb') as r:
            #await 非同步等待檔案讀取 type:bytes
            #此處用於逆操作解密檔案
                filedata = r.read()
                hashcode = api.get_user_hashcode(uid=uid)
                #args: usetype(encode/decode):str,data[userid,hash,fileid,file:bytes])
                decode_data = api.aestool_func(usetype='decode',data=[uid,hashcode,reqbody['filename'],filedata])
                r.close()
            with open(f"user/{uid}/origin_{reqbody['filename']}",'wb') as f:
            #將解密後的bytes寫入到一份臨時檔案,並且推送回客戶端,當客戶端完成下載後,請求rmFile介面移除臨時檔案
                f.write(decode_data)
                f.close()
            #構建file response返回體
            fr = FileResponse(
                path=f"user/{uid}/origin_{reqbody['filename']}",
                filename=reqbody['filename']
            )
            #返回檔案返回體
            return fr
        '''except Exception as err:
            return {
                'code':-5,
                'message':'token失效或檔案不存在或賬戶不存在\n'+str(err)
            }'''
    
    @server.post('/deleteFiles')
    async def delete(reqbody=Body(...)):
        #此介面允許使用者在完成身份校驗之後刪除自己已有的指定檔案
        #作用範圍僅限於user/{username}/file目錄下
        with open(f"user/{reqbody['uid']}/{reqbody['uid']}.pickle",'rb') as ck:
            fl = pickle.load(ck)
            ck.close()
        uid = reqbody['uid']
        token = reqbody['token']
        if not token == fl['token']:
            return {
                'code':-5,
                'message':'token失效或賬戶不存在'
            }
        else:
            try:
            #完成令牌可用心校驗後呼叫os.remove方法移出檔案
                os.remove(f"user/{reqbody['uid']}/file/{reqbody['filename']}")
                os.remove(f"RSA\{reqbody['uid']}_RSAprivate_{reqbody['filename']}.pem")
                os.remove(f"user\{uid}\{uid}_aeskey_{reqbody['filename']}.pickle")
            except Exception as err:
            #如果remove方法報錯,則返回錯誤資訊
                return {
                'code':-5,
                'message':f'移除失敗:{err}'
            }
            #若未觸發except則返回success表達移除成功
            return "Success"
    

    @server.post('/renameFiles')
    async def rename(reqbody=Body(...)):
        #此介面允許使用者在完成身份校驗後,對指定檔案進行重新命名
        with open(f"user/{reqbody['uid']}/{reqbody['uid']}.pickle",'rb') as ck:
            fl = pickle.load(ck)
            ck.close()
        uid = reqbody['uid']
        token = reqbody['token']
        if not token == fl['token']:
            return {
                'code':-5,
                'message':'token失效或賬戶不存在'
            }
        else:
            try:
            #身份校驗透過後則重新命名指定檔案與其對應的aes金鑰檔案
                os.rename(f"user/{reqbody['uid']}/file/{reqbody['filename']}",f"user/{reqbody['uid']}/file/{reqbody['update']}")
                os.rename(f"RSA\{reqbody['uid']}_RSAprivate_{reqbody['filename']}.pem",f"RSA\{reqbody['uid']}_RSAprivate_{reqbody['update']}.pem")
                os.rename(f"user/{reqbody['uid']}/{reqbody['uid']}_aeskey_{reqbody['filename']}.pickle",f"user/{reqbody['uid']}/{reqbody['uid']}_aeskey_{reqbody['update']}.pickle")
                old_keydata = api.get_user_aeskey_oridata(reqbody['uid'],reqbody['update'])[reqbody['filename']]
                #此處開啟舊的aes金鑰檔案,並且將更新後的檔名寫入,金鑰照舊,否則無法解密檔案
                with open(f"user\{uid}\{uid}_aeskey_{reqbody['update']}.pickle",'wb') as r:
                    pickle.dump({
                        reqbody['update']:old_keydata
                    },r)
                    r.close()
            except Exception as err:
                return {
                'code':-5,
                'message':f'修改失敗:{err}'
            }
            return "Success"

    @server.post('/login')
    async def login(reqbody=Body(...)):
        #此處為使用者登入介面,當賬號密碼可用,並且hash code校驗成功時,則給使用者推送token,並重新整理在服務端的令牌
        try:
            uid = reqbody['uid']
            req_hash = reqbody['hash']
        except Exception as   err:
            return {
                'code':-1,
                'message':f'請求body錯誤\n{err}'
            }
        try:
            with open(f'user/{uid}/{uid}.pickle','rb') as usrfile:
                userdata = pickle.load(usrfile)
                usrfile.close()
            #sha256校驗
            #由於其他介面的需求變動,請求頭則預設都附帶hashcode,則無需服務端再次生成
            hash_userpsw = userdata['hash']
            #req_userpsw = hashlib.sha256((str(uid)+str(password)).encode('utf-8')).hexdigest()
            if not hash_userpsw == req_hash:
                #雜湊校驗失敗時返回賬號或密碼錯誤
                return {
                    'code':-2,
                    'message':'帳號或密碼錯誤'
                }
            #雜湊校驗透過時則允許建立新的token並重新整理在服務端的令牌,且推送給使用者
            token = api.token_creator({
                    'uid':uid,
                    'hash':req_hash
                })
            #在使用者個人資訊檔案中重新整理token
            userdata['token'] = token
            newdata = userdata
            with open(f'user/{uid}/{uid}.pickle','wb') as dp:
                pickle.dump(newdata,dp)
                #儲存新的更新後檔案,更新內容僅為重新整理token,儲存關閉
                dp.close()
            return {
                'code':1,
                'message':'登入成功',
                'token':token
            }
        except Exception as err:
        #如果報錯則返回使用者不存在,具體包括為file not found error
            return {
                    'code':-3,
                    'message':f'此使用者不存在,請註冊-{err}'
                }

            
        

#封裝了一些複用的工具介面
class api(object):
    def __init__(self) -> None:
        #啟用一個執行緒用於執行webapi服務,並且初始化目錄
        if not os.path.exists('user/'):
            os.makedirs('user/')
            print('creater')
        #初始化結束後則使用theading建立執行FastAPI例項
        apithread = threading.Thread(target=self.runwebapi)
        apithread.start()


    def runwebapi(self) -> None:
        #此函式為執行緒承接函式,僅用於啟用api服務
        task = webserver()
        task.run()
    

    def get_user_hashcode(uid:str) -> str:
        #嘗試開啟指定使用者檔案獲取hash code,如果報錯為file not found error,則返回空
        try:
            with open(f'user/{uid}/{uid}.pickle','rb') as userfile:
                userdata_oncheck = pickle.load(userfile)
                userfile.close()
                return userdata_oncheck['hash']
        except:
            return None


    def token_creator(data:dict) -> str:
        #token生成函式 使用使用者uid pswd與時間戳的sha256值
        token_str_origin = f"{data['uid']}{data['hash']}{time.time()}"
        #此處的token生成規則為使用者ID拼接上hash code再拼接上此時的時間戳
        token = hashlib.sha256(token_str_origin.encode('utf-8')).hexdigest()
        #生成後返回
        return token
    

    def token_acceptuse(uid:str,token:str) -> bool:
        #此介面用於判斷指定使用者以及他的令牌可用性,若可用返回ture
        try:
            with open(f'user/{uid}/{uid}.pickle','rb') as userfile:
                userdata_oncheck = pickle.load(userfile)
                if not token == userdata_oncheck['token']:
                    userfile.close()
                    return False
                userfile.close()
                return True
        except:
            return False
    

    def get_free_space_mb(folder='C:\\'):
        """
        獲取磁碟剩餘空間
        :param folder: 磁碟路徑 例如 D:\\
        :return: 剩餘空間 單位 MB
        """
        #區分作業系統執行
        if platform.system() == 'Windows':
            free_bytes = ctypes.c_ulonglong(0)
            #ctypes的函式
            ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(folder), None, None, ctypes.pointer(free_bytes))
            #此處除以兩個1024表達返回剩餘可用mb
            return free_bytes.value / 1024 / 1024 
        else:
            st = os.statvfs(folder)
            return st.f_bavail * st.f_frsize / 1024 // 1024

    def get_user_aeskey_oridata(uid:str,filename:str) -> dict:
        #獲取aes金鑰檔案的原始資料字典
        try:
            with open(f"user\{uid}\{uid}_aeskey_{filename}.pickle",'rb') as r:
                userdata = pickle.load(r)
                r.close()
            return userdata
        except:
            return {}
    
    def save_user_aeskey(uid:str,filename:str,aeskey) -> bool:
        if not os.path.exists(f"RSA/"):
            os.makedirs(f"RSA/")
            #os.makedirs(f"RSA/SaveKeys/")
        #此函式用於快速儲存使用者上傳檔案加密後的aes金鑰檔案
        (public_key, private_key) = rsa.newkeys(2048)
        '''with open(f'RSA/{uid}_RSApublic_{filename}.pem', 'wb') as file:
            file.write(public_key.save_pkcs1())
            file.close()'''
        # 將私鑰儲存到PEM檔案中
        with open(f'RSA/{uid}_RSAprivate_{filename}.pem', 'wb') as file:
            file.write(private_key.save_pkcs1())
            file.close()
        rsa_output = rsa.encrypt(aeskey, public_key)
        with open(f"user\{uid}\{uid}_aeskey_{filename}.pickle",'wb') as w:
            oridata = api.get_user_aeskey_oridata(uid=uid,filename=filename)
            oridata[filename] = rsa_output
            pickle.dump(oridata,w)
            print(filename,'儲存加密後aeskey')
            w.close()
        return True


    def aestool_func(usetype:str,data:list):
        #防止陣列越界或超出限定
        if len(data) != 4:
            raise 'Data ERROR:len(data) != 4'
        #這個函式用於便捷處理aes加解密
        if usetype == 'encode':
            #data[userid,hash,fileid,file:bytes])
            key = bytes(secrets.token_hex(16),'utf-8')
            #ecb模式生成金鑰加密
            aes_key_ecb = AES.new(key,AES.MODE_ECB)
            #呼叫方法便捷儲存使用者aes金鑰檔案
            api.save_user_aeskey(uid=data[0],filename=data[2],aeskey=key)
            #獲取使用者加密後的bytes,encrypt函式中需要pad來補全aes需要的位寬,如不填滿位寬則無法加密
            encode_return = aes_key_ecb.encrypt(pad(data[3],AES.block_size))
            return encode_return
        elif usetype == 'decode':
        #如果使用模式是decode
            with open(f"user\{data[0]}\\file\\{data[2]}",'rb') as r:
                #讀出加密的檔案bytes
                orifile = r.read()
                r.close()
            #獲取金鑰檔案
            key = api.get_user_aeskey_oridata(data[0],filename=data[2])[data[2]]
            print(key)
            with open(f'RSA/{data[0]}_RSAprivate_{data[2]}.pem', 'rb') as file:
                private_key_data = file.read()
                private_key = rsa.PrivateKey.load_pkcs1(private_key_data)
                file.close()
            #新建一個解密物件
            decrypted_key = rsa.decrypt(key, private_key)
            print(decrypted_key)
            aes_key = AES.new(decrypted_key,AES.MODE_ECB)
            #解密檔案
            defile = aes_key.decrypt(orifile)
            #對加密時使用pad填充的位寬進行消除,使用unpad方法
            encode_return = unpad(defile,AES.block_size)
            return encode_return
        else:
            return None



    def getlocalip() -> str:
        #使用udp包來獲取本地ipv4,構建內網測試
        try:
            #新建一個socket物件
            stmp = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
            stmp.connect(('8.8.8.8',80))
            #對獲取的包頭進行提取,獲取到原始的IPV4,在部分情況下,比gethostname要穩定
            hostip = stmp.getsockname()[0]
        except Exception as err:
            print(f'error by {err}')
        finally:
            stmp.close()
        return hostip



if __name__ == '__main__':
    #啟動專案
    api()

UI_LoginClient.py

from PyQt5 import QtCore, QtGui, QtWidgets



class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(593, 317)
        self.backgroud = QtWidgets.QLabel(Form)
        self.backgroud.setGeometry(QtCore.QRect(0, 0, 595, 318))
        self.backgroud.setText("")
        self.backgroud.setObjectName("backgroud")
        self.title = QtWidgets.QLabel(Form)
        self.title.setGeometry(QtCore.QRect(0, 0, 611, 71))
        self.title.setObjectName("title")
        self.uidinput = QtWidgets.QLineEdit(Form)
        self.uidinput.setGeometry(QtCore.QRect(0, 130, 321, 41))
        self.uidinput.setObjectName("uidinput")
        self.passwordinput = QtWidgets.QLineEdit(Form)
        self.passwordinput.setGeometry(QtCore.QRect(0, 210, 321, 41))
        self.passwordinput.setObjectName("passwordinput")
        self.loginbutton = QtWidgets.QPushButton(Form)
        self.loginbutton.setGeometry(QtCore.QRect(390, 130, 131, 51))
        self.loginbutton.setObjectName("loginbutton")
        self.register = QtWidgets.QPushButton(Form)
        self.register.setGeometry(QtCore.QRect(390, 210, 131, 41))
        self.register.setObjectName("register")
        self.label = QtWidgets.QLabel(Form)
        self.label.setGeometry(QtCore.QRect(0, 110, 121, 16))
        self.label.setObjectName("label")
        self.label_2 = QtWidgets.QLabel(Form)
        self.label_2.setGeometry(QtCore.QRect(0, 190, 71, 16))
        self.label_2.setObjectName("label_2")

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "LoginClient"))
        self.title.setText(_translate("Form", "<html><head/><body><p align=\"center\"><span style=\" font-size:28pt; font-weight:1000; text-decoration: underline;\">歡迎使用雲伺服器加密系統</span></p><p align=\"center\"><span style=\" font-size:10pt; font-weight:500; text-decoration: underline;\">請登入</span></p></body></html>"))
        self.loginbutton.setText(_translate("Form", "登入"))
        self.register.setText(_translate("Form", "註冊"))
        self.label.setText(_translate("Form", "在此輸入賬號"))
        self.label_2.setText(_translate("Form", "在此輸入密碼"))

UI_MainClient.py

from PyQt5 import QtCore, QtGui, QtWidgets



class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(593, 317)
        self.backgroud = QtWidgets.QLabel(Form)
        self.backgroud.setGeometry(QtCore.QRect(0, 0, 595, 318))
        self.backgroud.setText("")
        self.backgroud.setObjectName("backgroud")
        self.title = QtWidgets.QLabel(Form)
        self.title.setGeometry(QtCore.QRect(0, 0, 611, 71))
        self.title.setObjectName("title")
        self.uidinput = QtWidgets.QLineEdit(Form)
        self.uidinput.setGeometry(QtCore.QRect(0, 130, 321, 41))
        self.uidinput.setObjectName("uidinput")
        self.passwordinput = QtWidgets.QLineEdit(Form)
        self.passwordinput.setGeometry(QtCore.QRect(0, 210, 321, 41))
        self.passwordinput.setObjectName("passwordinput")
        self.loginbutton = QtWidgets.QPushButton(Form)
        self.loginbutton.setGeometry(QtCore.QRect(390, 130, 131, 51))
        self.loginbutton.setObjectName("loginbutton")
        self.register = QtWidgets.QPushButton(Form)
        self.register.setGeometry(QtCore.QRect(390, 210, 131, 41))
        self.register.setObjectName("register")
        self.label = QtWidgets.QLabel(Form)
        self.label.setGeometry(QtCore.QRect(0, 110, 121, 16))
        self.label.setObjectName("label")
        self.label_2 = QtWidgets.QLabel(Form)
        self.label_2.setGeometry(QtCore.QRect(0, 190, 71, 16))
        self.label_2.setObjectName("label_2")

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "LoginClient"))
        self.title.setText(_translate("Form", "<html><head/><body><p align=\"center\"><span style=\" font-size:28pt; font-weight:1000; text-decoration: underline;\">歡迎使用雲伺服器加密系統</span></p><p align=\"center\"><span style=\" font-size:10pt; font-weight:500; text-decoration: underline;\">請登入</span></p></body></html>"))
        self.loginbutton.setText(_translate("Form", "登入"))
        self.register.setText(_translate("Form", "註冊"))
        self.label.setText(_translate("Form", "在此輸入賬號"))
        self.label_2.setText(_translate("Form", "在此輸入密碼"))

statistics.sh

#!/bin/sh

# rocedu (http://www.cnblogs.com/rocedu)
# Wechat Public Number: rocedu

clear
echo "//=====Today:====================================="
echo "code summary information:"
find . -name "*.py" -mtime 0 | xargs cat | grep -v ^$ | wc -l 
echo "documents summary information:"
find . -name "*.md" -mtime 0 | xargs cat | grep -v ^$ | wc -l 
echo ""

echo "//=====This Week:================================="
echo "code summary information:"
find . -name "*.py" -mtime -7| xargs cat | grep -v ^$ | wc -l 
echo "documents summary information:"
find . -name "*.md" -mtime -7| xargs cat | grep -v ^$ | wc -l 
git log --pretty=format:"%h - %an,%ci: %s " | grep  `date +%F --date="-0 days"`
git log --pretty=format:"%h - %an,%ci: %s " | grep  `date +%F --date="-1 days"`
git log --pretty=format:"%h - %an,%ci: %s " | grep  `date +%F --date="-2 days"`
git log --pretty=format:"%h - %an,%ci: %s " | grep  `date +%F --date="-3 days"`
git log --pretty=format:"%h - %an,%ci: %s " | grep  `date +%F --date="-4 days"`
git log --pretty=format:"%h - %an,%ci: %s " | grep  `date +%F --date="-5 days"`
git log --pretty=format:"%h - %an,%ci: %s " | grep  `date +%F --date="-6 days"`
echo ""
echo ""

echo "//=====This Semester:=============================="
echo "code summary information:"
find . -name "*.py"| xargs cat | grep -v ^$ | wc -l 
echo "documents summary information:"
find . -name "*.md"| xargs cat | grep -v ^$ | wc -l 
echo "commit history:"
git log --pretty=format:"%h - %an,%ci: %s "