commit 2240204c5e77806f1cb965eea562480662c5c7df Author: pinb Date: Wed Jul 3 15:45:35 2024 +0900 init diff --git a/bunnysync.py b/bunnysync.py new file mode 100644 index 0000000..51ae4bb --- /dev/null +++ b/bunnysync.py @@ -0,0 +1,16 @@ +import sys +from PyQt5.QtWidgets import QApplication +from main_window import MainWindow +from database import create_tables, delete_old_logs + +if __name__ == '__main__': + create_tables() + delete_old_logs() + + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec_()) + + +# pyinstaller --onefile bunnysync.py --add-data "images/realrabbit.png;images" --add-data "images/realrabbit2.png;images" --icon=images/realrabbit.png --noconsole \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..daf07d0 --- /dev/null +++ b/database.py @@ -0,0 +1,87 @@ +import sqlite3 +from datetime import datetime, timedelta + +DB_NAME = 'my_data.db' + +def get_db_conn(): + conn = sqlite3.connect(DB_NAME) + return conn + +def create_tables(): + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute('''CREATE TABLE IF NOT EXISTS jobs ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + folder1 TEXT NOT NULL, + folder2 TEXT NOT NULL, + interval INTEGER NOT NULL + )''') + cursor.execute('''CREATE TABLE IF NOT EXISTS logs ( + id INTEGER PRIMARY KEY, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + message TEXT NOT NULL + )''') + conn.commit() + conn.close() + +def add_job_to_db(name, folder1, folder2, interval): + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute('INSERT INTO jobs (name, folder1, folder2, interval) VALUES (?, ?, ?, ?)', (name, folder1, folder2, interval)) + conn.commit() + conn.close() + +def get_jobs_from_db(): + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute('SELECT * FROM jobs') + jobs = cursor.fetchall() + conn.close() + return [{'name': job[1], 'folder1': job[2], 'folder2': job[3], 'interval': job[4]} for job in jobs] + +def update_job_in_db(old_name, new_name, folder1, folder2, interval): + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute(''' + UPDATE jobs SET name = ?, folder1 = ?, folder2 = ?, interval = ? WHERE name = ? + ''', (new_name, folder1, folder2, interval, old_name)) + conn.commit() + conn.close() + +def delete_job_from_db(name): + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute('DELETE FROM jobs WHERE name = ?', (name,)) + cursor.execute(f"DROP TABLE IF EXISTS {name}") + conn.commit() + conn.close() + +def add_log_to_db(message): + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute('INSERT INTO logs (message) VALUES (?)', (message,)) + conn.commit() + conn.close() + +def get_logs_from_db(): + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + # cursor.execute('SELECT * FROM logs') + cursor.execute(''' + SELECT * + FROM logs + ORDER BY timestamp DESC + LIMIT 30 + ''') + logs = cursor.fetchall() + conn.close() + return [(log[1], log[2]) for log in logs] + +def delete_old_logs(): + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + one_month_ago = datetime.now() - timedelta(days=30) + cursor.execute('DELETE FROM logs WHERE timestamp < ?', (one_month_ago,)) + conn.commit() + conn.close() diff --git a/images/realrabbit.png b/images/realrabbit.png new file mode 100644 index 0000000..bd7217c Binary files /dev/null and b/images/realrabbit.png differ diff --git a/images/realrabbit2.png b/images/realrabbit2.png new file mode 100644 index 0000000..a0c4cba Binary files /dev/null and b/images/realrabbit2.png differ diff --git a/info_widget.py b/info_widget.py new file mode 100644 index 0000000..7dc02de --- /dev/null +++ b/info_widget.py @@ -0,0 +1,35 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy +from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import Qt +import sys, os + +class InfoWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout() + self.setLayout(layout) + + image_label = QLabel() + image_path = self.resource_path("images/realrabbit2.png") + pixmap = QPixmap(image_path) + scaled_pixmap = pixmap.scaled(400,500, Qt.KeepAspectRatio, Qt.SmoothTransformation) + image_label.setPixmap(scaled_pixmap) + image_label.setAlignment(Qt.AlignCenter) + layout.addWidget(image_label) + + self.info_label = QLabel(''' + This is \'Bunny Sync\' Application !\n + Thank you for using the app 🤗\n + App Version 1.0\n + Made by PINB\n + ''') + self.info_label.setAlignment(Qt.AlignCenter) + layout.addWidget(self.info_label) + + def resource_path(self, relative_path): + if hasattr(sys, '_MEIPASS'): + return os.path.join(sys._MEIPASS, relative_path) + return os.path.join(os.path.abspath("."), relative_path) \ No newline at end of file diff --git a/job_item.py b/job_item.py new file mode 100644 index 0000000..5250138 --- /dev/null +++ b/job_item.py @@ -0,0 +1,75 @@ +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton +from job_thread import JobThread + +class JobItem(QWidget): + def __init__(self, name, folder1, folder2, interval): + super().__init__() + self.name = name + self.folder1 = folder1 + self.folder2 = folder2 + self.interval = interval + self.thread = JobThread(self) + self.setup_ui() + + def setup_ui(self): + layout = QHBoxLayout() + self.setLayout(layout) + + self.name_label = QLabel(self.name) + self.name_label.setObjectName('jobitem') + self.play_button = QPushButton("Play") + self.sync_button = QPushButton("Sync") + self.delete_button = QPushButton("Delete") + + layout.addWidget(self.name_label) + layout.addWidget(self.play_button) + layout.addWidget(self.sync_button) + layout.addWidget(self.delete_button) + + self.setup_connections() + + def setup_connections(self): + self.play_button.clicked.connect(self.toggle_play) + self.sync_button.clicked.connect(self.run_sync) + self.thread.current_log.connect(self.update_log) + + def toggle_play(self): + if self.thread.is_running: + self.thread.stop() + self.play_button.setText("Play") + self.play_button.setStyleSheet(''' + QPushButton { + background-color: #558DED; + color: white; + border: none; + padding: 5px 10px; + border-radius: 5px; + font-size: 14px; + } + QPushButton:hover { + background-color: #55ED86; + } + ''') + else: + self.thread.start() + self.play_button.setText("Pause") + self.play_button.setStyleSheet(''' + QPushButton { + background-color: #55ED86; + color: white; + border: none; + padding: 5px 10px; + border-radius: 5px; + font-size: 14px; + } + QPushButton:hover { + background-color: #558DED; + } + ''') + + def run_sync(self): + if not self.thread.is_running: + self.thread.sync() + + def update_log(self, msg, fg, bg): + self.window().add_log(msg, fg, bg) diff --git a/job_list_widget.py b/job_list_widget.py new file mode 100644 index 0000000..759152c --- /dev/null +++ b/job_list_widget.py @@ -0,0 +1,171 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem +from PyQt5.QtGui import QColor +from PyQt5.QtCore import Qt +from job_item import JobItem +from database import add_job_to_db, get_jobs_from_db, update_job_in_db, delete_job_from_db + +class JobListWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + self.load_jobs_from_db() + + def setup_ui(self): + layout = QVBoxLayout() + self.setLayout(layout) + + job_info_widget = QWidget() + job_info_layout = QVBoxLayout() + job_info_widget.setLayout(job_info_layout) + + name_layout = QHBoxLayout() + name_label = QLabel("Job Name:") + name_label.setFixedSize(150, 35) + name_label.setObjectName('jobset') + name_label.setAlignment(Qt.AlignCenter) + name_layout.addWidget(name_label) + self.job_name_edit = QLineEdit() + name_layout.addWidget(self.job_name_edit) + + folder1_layout = QHBoxLayout() + folder1_label = QLabel("Folder 1:") + folder1_label.setFixedSize(150, 35) + folder1_label.setObjectName('jobset') + folder1_label.setAlignment(Qt.AlignCenter) + folder1_layout.addWidget(folder1_label) + self.folder1_edit = QLineEdit() + folder1_layout.addWidget(self.folder1_edit) + + folder2_layout = QHBoxLayout() + folder2_label = QLabel("Folder 2:") + folder2_label.setFixedSize(150, 35) + folder2_label.setObjectName('jobset') + folder2_label.setAlignment(Qt.AlignCenter) + folder2_layout.addWidget(folder2_label) + self.folder2_edit = QLineEdit() + folder2_layout.addWidget(self.folder2_edit) + + interval_layout = QHBoxLayout() + interval_label = QLabel("Interval:") + interval_label.setFixedSize(150, 35) + interval_label.setObjectName('jobset') + interval_label.setAlignment(Qt.AlignCenter) + interval_layout.addWidget(interval_label) + self.interval_edit = QLineEdit() + interval_layout.addWidget(self.interval_edit) + + buttons_layout = QHBoxLayout() + self.new_button = QPushButton("New") + self.new_button.clicked.connect(self.new_job) + buttons_layout.addWidget(self.new_button) + + self.add_apply_button = QPushButton("Add") + self.add_apply_button.clicked.connect(self.add_job) + buttons_layout.addWidget(self.add_apply_button) + + self.apply_button = QPushButton("Apply") + self.apply_button.clicked.connect(self.apply_job) + buttons_layout.addWidget(self.apply_button) + + job_info_layout.addLayout(name_layout) + job_info_layout.addLayout(folder1_layout) + job_info_layout.addLayout(folder2_layout) + job_info_layout.addLayout(interval_layout) + job_info_layout.addLayout(buttons_layout) + + layout.addWidget(job_info_widget) + + self.job_list = QListWidget() + self.job_list.itemClicked.connect(self.show_job_info) + layout.addWidget(self.job_list) + + def new_job(self): + self.job_name_edit.clear() + self.folder1_edit.clear() + self.folder2_edit.clear() + self.interval_edit.clear() + self.window().add_log('Clicked \'New Job\' !', QColor('yellow'), QColor('green')) + + def add_job(self): + name = self.job_name_edit.text() + folder1 = self.folder1_edit.text() + folder2 = self.folder2_edit.text() + interval = self.interval_edit.text() + + if name and folder1 and folder2 and interval: + # Check if job name already exists + if name == 'jobs' or name == 'logs' or name == 'sqlite_sequence': + self.window().add_log(f"Job with name '{name}' can not create.", QColor('white'), QColor('red')) + return + if any(self.job_list.itemWidget(self.job_list.item(i)).name == name for i in range(self.job_list.count())): + self.window().add_log(f"Job with name '{name}' already exists.", QColor('white'), QColor('red')) + return + + job_item = JobItem(name, folder1, folder2, int(interval)) + list_item = QListWidgetItem(self.job_list) + list_item.setSizeHint(job_item.sizeHint()) + self.job_list.addItem(list_item) + self.job_list.setItemWidget(list_item, job_item) + + job_item.delete_button.clicked.connect(lambda checked, item=list_item: self.delete_job(item)) + + add_job_to_db(name, folder1, folder2, int(interval)) + self.window().add_log(f'Clicked \'Add Job\' - ({job_item.name}) !', QColor('yellow'), QColor('green')) + + def delete_job(self, list_item): + job_item = self.job_list.itemWidget(list_item) + if job_item.thread.is_running: + job_item.thread.stop() + + self.job_list.takeItem(self.job_list.row(list_item)) + delete_job_from_db(job_item.name) + self.window().add_log(f'Clicked \'delete Job\' - ({job_item.name}) !', QColor('red'), QColor('green')) + + def show_job_info(self, item): + job_item = self.job_list.itemWidget(item) + self.job_name_edit.setText(job_item.name) + self.folder1_edit.setText(job_item.folder1) + self.folder2_edit.setText(job_item.folder2) + self.interval_edit.setText(str(job_item.interval)) + + def apply_job(self): + self.selected_items = self.job_list.selectedItems() + if self.selected_items: + job_item = self.job_list.itemWidget(self.selected_items[0]) + old_name = job_item.name + + new_name = self.job_name_edit.text() + folder1 = self.folder1_edit.text() + folder2 = self.folder2_edit.text() + interval = self.interval_edit.text() + + if new_name and folder1 and folder2 and interval: + # Check if new job name already exists in other jobs + if old_name != new_name and any(self.job_list.itemWidget(self.job_list.item(i)).name == new_name for i in range(self.job_list.count())): + self.window().add_log(f"Job with name '{old_name}' already exists.", QColor('white'), QColor('red')) + return + + # Update the job item + job_item.name = new_name + job_item.folder1 = folder1 + job_item.folder2 = folder2 + job_item.interval = int(interval) + job_item.name_label.setText(new_name) + + # Update the database + update_job_in_db(old_name, new_name, folder1, folder2, int(interval)) + + self.window().add_log(f'Clicked \'Apply Job\' - ({job_item.name}) !', QColor('yellow'), QColor('green')) + + def load_jobs_from_db(self): + jobs = get_jobs_from_db() + for job in jobs: + job_item = JobItem(job['name'], job['folder1'], job['folder2'], job['interval']) + list_item = QListWidgetItem(self.job_list) + list_item.setSizeHint(job_item.sizeHint()) + self.job_list.addItem(list_item) + self.job_list.setItemWidget(list_item, job_item) + + job_item.delete_button.clicked.connect(lambda checked, item=list_item: self.delete_job(item)) + + diff --git a/job_thread.py b/job_thread.py new file mode 100644 index 0000000..e9b3b0e --- /dev/null +++ b/job_thread.py @@ -0,0 +1,30 @@ +from PyQt5.QtCore import QThread, pyqtSignal +from PyQt5.QtGui import QColor +import time +from sync import FolderSynchronizer + +class JobThread(QThread): + current_log = pyqtSignal(str, QColor, QColor) + + def __init__(self, job_item): + super().__init__() + self.name = job_item.name + self.folder1 = job_item.folder1 + self.folder2 = job_item.folder2 + self.interval = job_item.interval + self.is_running = False + self.synchronizer = FolderSynchronizer(self.name, self.folder1, self.folder2, self.interval) + + def run(self): + self.is_running = True + while self.is_running: + log = self.synchronizer.sync_folders() + self.current_log.emit(log, QColor("white"), QColor("green")) + time.sleep(self.interval) + + def stop(self): + self.is_running = False + + def sync(self): + log = self.synchronizer.sync_folders() + self.current_log.emit(log, QColor("yellow"), QColor("green")) \ No newline at end of file diff --git a/log_widget.py b/log_widget.py new file mode 100644 index 0000000..83e61e0 --- /dev/null +++ b/log_widget.py @@ -0,0 +1,93 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QTextEdit, QPushButton +from PyQt5.QtGui import QTextCursor, QTextBlockFormat, QTextCharFormat, QColor +from database import add_log_to_db, get_logs_from_db +from datetime import datetime + +class LogWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + self.load_logs_from_db() + + def setup_ui(self): + layout = QVBoxLayout() + self.setLayout(layout) + + self.log_text = QTextEdit() + self.log_text.setReadOnly(True) + layout.addWidget(self.log_text) + + self.log_input = QLineEdit() + self.log_input.setPlaceholderText("Enter log message") + layout.addWidget(self.log_input) + + buttons_layout = QHBoxLayout() + log_button = QPushButton("Add Log") + log_button.clicked.connect(self.add_uilog) + buttons_layout.addWidget(log_button) + clear_button = QPushButton("Clear Log") + clear_button.clicked.connect(self.clear_log) + buttons_layout.addWidget(clear_button) + + buttons_widget = QWidget() + buttons_widget.setLayout(buttons_layout) + layout.addWidget(buttons_widget) + + def get_line_count(self): + document = self.log_text.document() + return document.blockCount() + + def delete_first_line(self): + cursor = self.log_text.textCursor() + cursor.movePosition(QTextCursor.Start) + cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) + cursor.removeSelectedText() + self.log_text.setTextCursor(cursor) + + def delete_lines(self, n): + cursor = self.log_text.textCursor() + cursor.movePosition(QTextCursor.Start) + + for _ in range(n): + if cursor.atEnd(): + break + cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) + cursor.removeSelectedText() + + self.log_text.setTextCursor(cursor) + + def clear_log(self): + self.log_text.clear() + + def update_log(self, message, foreground=QColor("black"), background=QColor("white")): + char_format = QTextCharFormat() + char_format.setForeground(foreground) + char_format.setBackground(background) + + block_format = QTextBlockFormat() + block_format.setBackground(background) + + self.log_text.moveCursor(QTextCursor.End) + cursor = self.log_text.textCursor() + cursor.insertBlock(block_format) + cursor.insertText(message, char_format) + self.log_text.setTextCursor(cursor) + + def add_log(self, message, foreground=QColor("black"), background=QColor("white")): + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + log_message = f"[{timestamp}] {message}" + self.update_log(log_message, foreground, background) + add_log_to_db(message) + self.log_input.clear() + if 3000 < self.get_line_count(): + self.delete_first_line() + + def add_uilog(self): + message = self.log_input.text() + self.add_log(message) + + def load_logs_from_db(self): + logs = get_logs_from_db() + for timestamp, message in logs: + log_message = f"[{timestamp}] {message}" + self.update_log(log_message, QColor("dark"), QColor("gray")) \ No newline at end of file diff --git a/main_window.py b/main_window.py new file mode 100644 index 0000000..ef12d04 --- /dev/null +++ b/main_window.py @@ -0,0 +1,129 @@ +from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QLabel +from PyQt5.QtGui import QColor, QIcon +from job_list_widget import JobListWidget +from log_widget import LogWidget +from info_widget import InfoWidget +import sys, os + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Bunny Sync Application") + self.setGeometry(100, 100, 800, 600) + image_path = self.resource_path("images/realrabbit.png") + self.setWindowIcon(QIcon(image_path)) + self.setup_ui() + + def resource_path(self, relative_path): + if hasattr(sys, '_MEIPASS'): + return os.path.join(sys._MEIPASS, relative_path) + return os.path.join(os.path.abspath("."), relative_path) + + def setup_ui(self): + main_widget = QWidget() + main_layout = QHBoxLayout() + main_widget.setLayout(main_layout) + self.setCentralWidget(main_widget) + + left_panel = QWidget() + left_layout = QVBoxLayout() + left_panel.setLayout(left_layout) + left_panel.setObjectName('menu') + left_panel.setFixedWidth(80) + + self.list_button = QPushButton("List") + self.log_button = QPushButton("Log") + self.info_button = QPushButton("Info") + left_layout.addWidget(self.list_button) + left_layout.addWidget(self.log_button) + left_layout.addWidget(self.info_button) + left_layout.addStretch() + + self.right_panel = QWidget() + self.right_layout = QVBoxLayout() + self.right_panel.setLayout(self.right_layout) + + main_layout.addWidget(left_panel) + main_layout.addWidget(self.right_panel) + + self.job_list_widget = JobListWidget(self) + self.log_widget = LogWidget(self) + self.info_widget = InfoWidget(self) + + self.right_layout.addWidget(self.job_list_widget) + self.right_layout.addWidget(self.log_widget) + self.right_layout.addWidget(self.info_widget) + + self.list_button.clicked.connect(self.show_job_list) + self.log_button.clicked.connect(self.show_log) + self.info_button.clicked.connect(self.show_info) + + self.setStyleSheet(""" + QWidget { + background-color: #C8F0F0; + border-radius: 10px; + padding: 10px; + } + QTextEdit { + background-color: #ffffff; + color: #333333; + border-radius: 10px; + padding: 10px; + } + QLineEdit { + background-color: #56BFF0; + color: #333333; + border-radius: 10px; + padding: 10px; + } + QLabel { + color: #333333; + font-size: 14px; + } + QLabel#jobset { + font-size: 14px; + font-weight: bold; + } + QLabel#jobitem { + background-color: #ffffff; + font-size: 10px; + font-weight: bold; + } + QPushButton { + background-color: #558DED; + color: white; + border: none; + padding: 5px 10px; + border-radius: 5px; + font-size: 14px; + } + QPushButton:hover { + background-color: #55ED86; + } + QListWidget { + background-color: #56BFF0; + } + QWidget#menu { + background-color: #55EDED; + } + """) + + self.show_job_list() + + def show_job_list(self): + self.job_list_widget.show() + self.log_widget.hide() + self.info_widget.hide() + + def show_log(self): + self.job_list_widget.hide() + self.log_widget.show() + self.info_widget.hide() + + def show_info(self): + self.job_list_widget.hide() + self.log_widget.hide() + self.info_widget.show() + + def add_log(self, message, foreground=QColor("black"), background=QColor("white")): + self.log_widget.add_log(message, foreground, background) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8cf979c Binary files /dev/null and b/requirements.txt differ diff --git a/sync.py b/sync.py new file mode 100644 index 0000000..44ec267 --- /dev/null +++ b/sync.py @@ -0,0 +1,151 @@ +import os +import shutil +import time +import datetime +import sqlite3 +import threading + +DB_NAME = 'my_data.db' + +class FolderSynchronizer: + def __init__(self, name, folder1, folder2, interval): + self.name = name + self.folder1 = folder1 + self.folder2 = folder2 + self.interval = interval + self.local = threading.local() + self.create_table() + + def get_connection(self): + if not hasattr(self.local, 'conn'): + self.local.conn = sqlite3.connect(DB_NAME) + return self.local.conn + + def get_cursor(self): + return self.get_connection().cursor() + + def create_table(self): + conn = self.get_connection() + cursor = conn.cursor() + cursor.execute(f''' + CREATE TABLE IF NOT EXISTS {self.name} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rel_dir TEXT, + file_name TEXT, + modified1 REAL, + exist1 INTEGER, + modified2 REAL, + exist2 INTEGER, + UNIQUE(rel_dir, file_name) + ) + ''') + conn.commit() + + # def delete_job(self): + # conn = self.get_connection() + # cursor = conn.cursor() + # cursor.execute(f"DROP TABLE IF EXISTS {self.name}") + # conn.commit() + # print(f"Job '{self.name}' has been deleted.") + + def update_db(self, rel_dir, file_name, modified1, exist1, modified2, exist2): + conn = self.get_connection() + cursor = conn.cursor() + cursor.execute(f''' + INSERT OR REPLACE INTO {self.name} + (rel_dir, file_name, modified1, exist1, modified2, exist2) + VALUES (?, ?, ?, ?, ?, ?) + ''', (rel_dir, file_name, modified1, exist1, modified2, exist2)) + conn.commit() + + def get_file_info(self, rel_dir, file_name): + conn = self.get_connection() + cursor = conn.cursor() + cursor.execute(f''' + SELECT * FROM {self.name} + WHERE rel_dir = ? AND file_name = ? + ''', (rel_dir, file_name)) + return cursor.fetchone() + + def sync_folders(self): + def sync_files(src_folder, dest_folder): + current_time = time.time() + total_sync = 0 + count_sync = 0 + for root, _, files in os.walk(src_folder): + for filename in files: + src_path = os.path.join(root, filename) + rel_dir = os.path.relpath(root, src_folder) + dest_path = os.path.join(dest_folder, rel_dir, filename) + total_sync += 1 + + src_modified = os.path.getmtime(src_path) + file_info = self.get_file_info(rel_dir, filename) + + if not file_info: + # New file + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + shutil.copy2(src_path, dest_path) + count_sync += 1 + dest_modified = current_time + modified1, exist1, modified2, exist2 = src_modified, 1, dest_modified, 1 + else: + # Existing file + if src_modified > file_info[3] or not file_info[4]: + shutil.copy2(src_path, dest_path) + count_sync += 1 + dest_modified = current_time + modified1, exist1, modified2, exist2 = src_modified, 1, dest_modified, 1 + else: + modified1, exist1, modified2, exist2 = src_modified, 1, file_info[5], file_info[6] + + self.update_db(rel_dir, filename, modified1, exist1, modified2, exist2) + + return total_sync, count_sync + + def check_deleted_files(): + conn = self.get_connection() + cursor = conn.cursor() + current_time = time.time() + count_sync = 0 + cursor.execute(f"SELECT rel_dir, file_name, modified1, exist1, modified2, exist2 FROM {self.name}") + for row in cursor.fetchall(): + rel_dir, file_name, modified1, exist1, modified2, exist2 = row + path1 = os.path.join(self.folder1, rel_dir, file_name) + path2 = os.path.join(self.folder2, rel_dir, file_name) + + if not os.path.exists(path1) and not os.path.exists(path2): + count_sync += 1 + cursor.execute(f"DELETE FROM {self.name} WHERE rel_dir = ? AND file_name = ?", (rel_dir, file_name)) + elif not os.path.exists(path1) and exist1: + modified1 = current_time + if modified1 > modified2: + count_sync += 1 + os.remove(path2) + cursor.execute(f"DELETE FROM {self.name} WHERE rel_dir = ? AND file_name = ?", (rel_dir, file_name)) + else: + self.update_db(rel_dir, file_name, modified1, 0, modified2, exist2) + elif not os.path.exists(path2) and exist2: + modified2 = current_time + if modified2 > modified1: + count_sync += 1 + os.remove(path1) + cursor.execute(f"DELETE FROM {self.name} WHERE rel_dir = ? AND file_name = ?", (rel_dir, file_name)) + else: + self.update_db(rel_dir, file_name, modified1, exist1, modified2, 0) + + conn.commit() + return count_sync + + total_sync1, count_sync1 = sync_files(self.folder1, self.folder2) + total_sync2, count_sync2 = sync_files(self.folder2, self.folder1) + count_sync3 = check_deleted_files() + + total_sync = max(total_sync1, total_sync2) + count_sync = count_sync1 + count_sync2 + count_sync3 + + # now = datetime.datetime.now() + # formatted_datetime = now.strftime("[%Y-%m-%d %H:%M:%S]") + + # return f"[{self.name}] \'{self.folder1} <-> {self.folder2}\' Sync completed! (sync count: {count_sync}, total files: {total_sync})" + return f"[{self.name}] Sync completed! (sync count: {count_sync}, total files: {total_sync})" \ No newline at end of file