Initial commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/BackupCopier.spec
|
||||||
|
/dist/
|
||||||
|
/build/
|
||||||
|
/.venv/
|
||||||
77
build.py
Normal file
77
build.py
Normal 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}")
|
||||||
1
requirements-dev.txt
Normal file
1
requirements-dev.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pytest>=8.0
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
schedule>=1.2
|
||||||
|
pystray>=0.19
|
||||||
|
pillow>=10.0
|
||||||
95
tests/test_copy_logic.py
Normal file
95
tests/test_copy_logic.py
Normal 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
|
||||||
Reference in New Issue
Block a user