1 The Problem
We want a file sync tool (a tiny rsync): make a destination folder match a source, but efficiently — only copying files that are new or changed, skipping identical ones. The trick is detecting change with checksums. It teaches efficient change detection, the core of backup and sync tools.
2 How to Think About It
Think about how to know a file changed, before any code:
3 The Build — explained part by part
Here is the complete sync tool. The change-detection logic is testable on its own. Each part is explained below.
import hashlib
import shutil
from pathlib import Path
def checksum(path):
# A short fingerprint of a file's contents.
h = hashlib.md5()
h.update(Path(path).read_bytes())
return h.hexdigest()
def needs_copy(source, dest):
# Copy if the destination is missing or different.
if not Path(dest).exists():
return True
return checksum(source) != checksum(dest)
def sync(source_dir, dest_dir):
source = Path(source_dir)
dest = Path(dest_dir)
dest.mkdir(exist_ok=True)
copied = []
for src_file in source.glob("*"):
if src_file.is_file():
dest_file = dest / src_file.name
if needs_copy(src_file, dest_file):
shutil.copy2(src_file, dest_file)
copied.append(src_file.name)
return copied
if __name__ == "__main__":
copied = sync("source", "backup")
print(f"Copied {len(copied)} changed files: {copied}")
needs_copy(source, dest) — the decision: if the destination does not exist, copy. If it does, compare checksums — copy only if they differ. This is the efficiency: identical files are skipped.
source.glob("*") — go through every item in the source folder;
is_file() skips sub-folders.shutil.copy2 — copy the file and its metadata (timestamps). We only reach this for files that actually need copying.
copied list — we record which files we copied, so the tool can report exactly what changed — useful and reassuring.
4 Test & Prove Each Part
We test the change-detection logic — the decision of whether to copy — which is the part that matters most.
import hashlib
import os
import tempfile
from pathlib import Path
def checksum(path):
h = hashlib.md5()
h.update(Path(path).read_bytes())
return h.hexdigest()
def needs_copy(source, dest):
if not Path(dest).exists():
return True
return checksum(source) != checksum(dest)
def make_file(content):
path = os.path.join(tempfile.mkdtemp(), "f.txt")
Path(path).write_text(content)
return path
def test_missing_dest_needs_copy():
"""A non-existent destination always needs copying."""
src = make_file("hello")
assert needs_copy(src, "/tmp/does-not-exist-xyz") is True
def test_identical_skipped():
"""Identical files are not copied."""
src = make_file("same content")
dest = make_file("same content")
assert needs_copy(src, dest) is False
def test_changed_detected():
"""A different destination is detected and copied."""
src = make_file("new content")
dest = make_file("old content")
assert needs_copy(src, dest) is True
Run with pytest -v. We test needs_copy with real temp files of known content. The identical-skipped test proves the efficiency works; the changed-detected test proves we never miss a real change.
5 The Interface
Call
sync("source", "backup")Returns
Copied 2 changed files: ['new.txt', 'updated.txt']
# identical files skipped6 Run It & Automate It
python3 filesync.pyRun it twice — the second run copies nothing because nothing changed. Change a file and it copies just that one.
Jenkins runs the tests automatically — each line explained below.
Copied 2 changed files: ['report.pdf', 'notes.txt']needs_copy.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Recursive sync. Handle sub-folders too. (Teaches: walking trees.)
- Delete extras. Remove destination files no longer in source. (Teaches: two-way comparison.)
- Dry run. Show what would change without copying. (Teaches: safe previews.)