Initial commit

This commit is contained in:
2026-02-19 19:06:21 +03:00
commit 36708967f7
7 changed files with 1365 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/BackupCopier.spec
/dist/
/build/
/.venv/

77
build.py Normal file
View File

@@ -0,0 +1,77 @@
import os
import shutil
import subprocess
import sys
import zipfile
APP_NAME = "BackupCopier"
VERSION = "1.0.0"
MAIN_SCRIPT = "main.py"
ICON_PATH = "icon.ico"
EXE_PATH = os.path.join("dist", f"{APP_NAME}.exe")
ZIP_PATH = os.path.join("dist", f"{APP_NAME}-{VERSION}.zip")
def ensure_build_deps() -> None:
try:
__import__("PyInstaller")
except Exception:
print("[ERROR] Missing dependency: PyInstaller")
print(f"[ERROR] Install in this interpreter: {sys.executable} -m pip install pyinstaller")
sys.exit(1)
def run_build() -> None:
icon_abs = os.path.abspath(ICON_PATH)
has_icon = os.path.exists(icon_abs)
cmd = [
sys.executable,
"-m",
"PyInstaller",
"--noconfirm",
"--clean",
"--onefile",
"--windowed",
f"--name={APP_NAME}",
"--hidden-import=schedule",
"--collect-submodules=schedule",
"--hidden-import=pystray",
"--hidden-import=PIL",
"--collect-submodules=pystray",
"--collect-submodules=PIL",
f"--icon={icon_abs}" if has_icon else "",
f"--add-data={icon_abs}{os.pathsep}." if has_icon else "",
MAIN_SCRIPT,
]
cmd = [x for x in cmd if x]
subprocess.check_call(cmd)
def write_version_file() -> str:
path = os.path.join("dist", "version.txt")
with open(path, "w", encoding="utf-8") as f:
f.write(VERSION + "\n")
return path
def create_zip(version_file: str) -> None:
with zipfile.ZipFile(ZIP_PATH, "w", zipfile.ZIP_DEFLATED) as z:
z.write(EXE_PATH, arcname=os.path.basename(EXE_PATH))
z.write(version_file, arcname="version.txt")
if os.path.exists(ICON_PATH):
z.write(ICON_PATH, arcname="icon.ico")
if __name__ == "__main__":
ensure_build_deps()
for folder in ("build", "dist"):
if os.path.exists(folder):
shutil.rmtree(folder)
run_build()
vfile = write_version_file()
create_zip(vfile)
print(f"[OK] Built: {EXE_PATH}")
print(f"[OK] Archive: {ZIP_PATH}")

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

1185
main.py Normal file

File diff suppressed because it is too large Load Diff

1
requirements-dev.txt Normal file
View File

@@ -0,0 +1 @@
pytest>=8.0

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
schedule>=1.2
pystray>=0.19
pillow>=10.0

95
tests/test_copy_logic.py Normal file
View File

@@ -0,0 +1,95 @@
import os
import time
from pathlib import Path
import pytest
from main import find_latest_file_in_folder, should_copy_file, compute_file_checksum, verify_copy
def _touch(path: Path, mtime: float) -> None:
path.write_text('data', encoding='utf-8')
os.utime(path, (mtime, mtime))
def test_find_latest_file_returns_none_for_missing_folder(tmp_path):
missing = tmp_path / 'missing'
assert find_latest_file_in_folder(str(missing)) is None
def test_find_latest_file_picks_newest(tmp_path):
folder = tmp_path / 'src'
folder.mkdir()
now = time.time()
older = folder / 'older.bak'
newer = folder / 'newer.bak'
other = folder / 'other.sql'
_touch(older, now - 10)
_touch(newer, now - 5)
_touch(other, now - 1)
latest = find_latest_file_in_folder(str(folder))
assert latest is not None
assert latest.name == 'other.sql'
def test_should_copy_file_when_target_missing(tmp_path):
src = tmp_path / 'src.bak'
src.write_text('x', encoding='utf-8')
dst = tmp_path / 'dst.bak'
assert should_copy_file(src, dst) is True
def test_should_copy_file_when_source_newer(tmp_path):
now = time.time()
src = tmp_path / 'src.bak'
dst = tmp_path / 'dst.bak'
_touch(dst, now - 10)
_touch(src, now)
assert should_copy_file(src, dst) is True
def test_should_copy_file_when_target_newer_or_equal(tmp_path):
now = time.time()
src = tmp_path / 'src.bak'
dst = tmp_path / 'dst.bak'
_touch(src, now)
_touch(dst, now)
assert should_copy_file(src, dst) is False
def test_compute_file_checksum_stable(tmp_path):
src = tmp_path / 'a.bak'
src.write_text('hello', encoding='utf-8')
c1 = compute_file_checksum(src)
c2 = compute_file_checksum(src)
assert c1 == c2
def test_verify_copy_true_for_equal_files(tmp_path):
src = tmp_path / 'src.bak'
dst = tmp_path / 'dst.bak'
src.write_text('data', encoding='utf-8')
dst.write_text('data', encoding='utf-8')
assert verify_copy(src, dst) is True
def test_verify_copy_false_for_different_files(tmp_path):
src = tmp_path / 'src.bak'
dst = tmp_path / 'dst.bak'
src.write_text('data1', encoding='utf-8')
dst.write_text('data2', encoding='utf-8')
assert verify_copy(src, dst) is False