Want to save snapshots of work in progress
Create a simple version control system
And show how to test it using mock objects (Chapter 9)
Wasteful to store the same file repeatedly
So if the file's hash is abcd1234, save it as abcd1234.bck
Then create a manifest to show what unique blocks of bytes had what names when
HASH_LEN = 16
def hash_all(root):
result = []
for name in glob("**/*.*", root_dir=root, recursive=True):
full_name = Path(root, name)
with open(full_name, "rb") as reader:
data = reader.read()
hash_code = sha256(data).hexdigest()[:HASH_LEN]
result.append((name, hash_code))
return result
sample_dir
|-- a.txt
|-- b.txt
`-- sub_dir
`-- c.txt
1 directory, 3 files
python hash_all.py sample_dir
filename,hash
b.txt,3cf9a1a81f6bdeaf
a.txt,17e682f060b5f8e4
sub_dir/c.txt,5695d82a086b6779
Obvious approach is to create lots of files and directories
But we want to test what happens when they change, which makes things complicated to maintain
Use a mock object (Chapter 9) instead of the real filesystem
open
with ones that behave the same way
but act on "files" stored in memoryimport pyfakefs automatically creates a fixture called fsfrom pathlib import Path
def test_simple_example(fs):
sentence = "This file contains one sentence."
with open("alpha.txt", "w") as writer:
writer.write(sentence)
assert Path("alpha.txt").exists()
with open("alpha.txt", "r") as reader:
assert reader.read() == sentence
from pathlib import Path
import pytest
@pytest.fixture
def our_fs(fs):
fs.create_file("a.txt", contents="aaa")
fs.create_file("b.txt", contents="bbb")
fs.create_file("sub_dir/c.txt", contents="ccc")
def test_nested_example(our_fs):
assert Path("a.txt").exists()
assert Path("b.txt").exists()
assert Path("sub_dir/c.txt").exists()
def test_deletion_example(our_fs):
assert Path("a.txt").exists()
Path("a.txt").unlink()
assert not Path("a.txt").exists()
import pytest
from hash_all import hash_all, HASH_LEN
@pytest.fixture
def our_fs(fs):
fs.create_file("a.txt", contents="aaa")
fs.create_file("b.txt", contents="bbb")
fs.create_file("sub_dir/c.txt", contents="ccc")
def test_hashing(our_fs):
result = hash_all(".")
expected = {"a.txt", "b.txt", "sub_dir/c.txt"}
assert {r[0] for r in result} == expected
assert all(len(r[1]) == HASH_LEN for r in result)
# ...8 lines not shown...
Manifest naming scheme fails if we try to create two backups in less than one second
May seem unlikely, but many bugs and security holes seemed unlikely to their creators
def backup(source_dir, backup_dir):
manifest = hash_all(source_dir)
timestamp = current_time()
write_manifest(backup_dir, timestamp, manifest)
copy_files(source_dir, backup_dir, manifest)
return manifest
Create the backup directory if it doesn't already exist
Then save CSV
def write_manifest(backup_dir, timestamp, manifest):
backup_dir = Path(backup_dir)
if not backup_dir.exists():
backup_dir.mkdir()
manifest_file = Path(backup_dir, f"{timestamp}.csv")
with open(manifest_file, "w") as raw:
writer = csv.writer(raw)
writer.writerow(["filename", "hash"])
writer.writerows(manifest)
def copy_files(source_dir, backup_dir, manifest):
for (filename, hash_code) in manifest:
source_path = Path(source_dir, filename)
backup_path = Path(backup_dir, f"{hash_code}.bck")
if not backup_path.exists():
shutil.copy(source_path, backup_path)
FILES = {"a.txt": "aaa", "b.txt": "bbb", "sub_dir/c.txt": "ccc"}
@pytest.fixture
def our_fs(fs):
for name, contents in FILES.items():
fs.create_file(name, contents=contents)
def test_nested_example(our_fs):
timestamp = 1234
with patch("backup.current_time", return_value=timestamp):
manifest = backup(".", "/backup")
assert Path("/backup", f"{timestamp}.csv").exists()
for filename, hash_code in manifest:
assert Path("/backup", f"{hash_code}.bck").exists()
Trust that the hash is correct
Should look inside the manifest and check that it lists files correctly
class Archive:
def __init__(self, source_dir):
self._source_dir = source_dir
def backup(self):
manifest = hash_all(self._source_dir)
self._write_manifest(manifest)
self._copy_files(manifest)
return manifest
Derive a child class to do local archiving
Convert functions we have built so far into methods
archiver = ArchiveLocal(source_dir, backup_dir)
def analyze_and_save(options, archiver):
data = read_data(options)
results = analyze_data(data)
save_everything(results)
archiver.backup()