From 6a1667e232f63f61b34a0ae8ed12932c0ab3779e Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Wed, 12 Aug 2026 01:05:46 -0700 Subject: [PATCH 1/2] Add script to create testdata ZIP for multi-file slide There are multiple footguns when doing it by hand: files could be set read-only, owner/group IDs could be included, compression could be disabled, or a top-level directory could be included (as is conventional for other kinds of dist archives). Add a script that creates the ZIP file correctly and also prints the resulting SHA-256 for convenience. Signed-off-by: Benjamin Gilbert --- _testdata/testdata_zip.py | 76 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100755 _testdata/testdata_zip.py diff --git a/_testdata/testdata_zip.py b/_testdata/testdata_zip.py new file mode 100755 index 0000000..f0be94c --- /dev/null +++ b/_testdata/testdata_zip.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# +# testdata_zip - Create ZIP archive for multi-file testdata slide +# +# Copyright (c) 2026 Benjamin Gilbert +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License +# as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# + +from __future__ import annotations + +import argparse +from hashlib import sha256 +from pathlib import Path +from shutil import copyfileobj +from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile, ZipInfo + + +def add_member(zf: ZipFile, root: Path, path: Path) -> None: + info = ZipInfo.from_file(path, path.relative_to(root)) + if info.is_dir(): + info.compress_type = ZIP_STORED + info.external_attr = (0o40755 << 16) | 0x10 + zf.writestr(info, b'') + else: + info.compress_type = ZIP_DEFLATED + info.compress_level = 9 + info.external_attr = 0o100644 << 16 + with path.open('rb') as rh, zf.open(info, 'w') as wh: + copyfileobj(rh, wh) + + +def create_zip(path: Path) -> None: + def on_error(err: OSError) -> None: + raise err + + zip_path = path.with_suffix('.zip') + with ZipFile(zip_path, 'w') as zf: + for dirpath, dirnames, filenames in path.walk(on_error=on_error): + dirnames.sort() + if dirpath != path: + add_member(zf, path, dirpath) + for filename in sorted(filenames): + add_member(zf, path, dirpath / filename) + + with zip_path.open('rb') as fh: + hash = sha256() + while True: + buf = fh.read(1 << 20) + if not buf: + break + hash.update(buf) + print(hash.hexdigest()) + + +def _main() -> None: + parser = argparse.ArgumentParser( + description='Create openslide-testdata ZIP from specified directory.' + ) + parser.add_argument('path', type=Path, help='path to source directory') + args = parser.parse_args() + create_zip(args.path) + + +if __name__ == '__main__': + _main() From 939de1c6ec5be317f6c404f4f6921b1b296a88c7 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Wed, 12 Aug 2026 01:38:57 -0700 Subject: [PATCH 2/2] testdata_index: sort version 2 testdata slides after version 1 `foo.zip` sorts after `foo-v2.zip`, but `foo` sorts before `foo-v2`. Sort HTML indexes on filename stems instead of entire filenames. Drop redundant sorting of directory names in top-level index. Signed-off-by: Benjamin Gilbert --- _testdata/testdata_index.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/_testdata/testdata_index.py b/_testdata/testdata_index.py index a489b3a..451fc08 100755 --- a/_testdata/testdata_index.py +++ b/_testdata/testdata_index.py @@ -24,7 +24,7 @@ from collections.abc import Iterable from hashlib import sha256 import json -from pathlib import Path +from pathlib import Path, PurePath from typing import Any from jinja2 import Environment @@ -123,10 +123,10 @@ {% if has_parent %} {{ row('fa-level-up', '..', '[Parent Directory]') }} {% endif %} - {% for name, format in (dirs or {}).items()|sort %} + {% for name, format in (dirs or {}).items() %} {{ row('fa-folder', name + '/', name, description=format) }} {% endfor %} - {% for name, info in (files or {}).items()|sort %} + {% for name, info in (files or {}).items() %} {{ row('fa-file-archive-o' if name.endswith('.zip') else 'fa-file-image-o', name, name, info.size|file_size_units, info.description, info.license, info.credit, info.deprecated) }} @@ -215,7 +215,9 @@ def process_dir( index_template.stream( has_parent=True, title=format_, - files=slides, + files=dict( + sorted(slides.items(), key=lambda p: PurePath(p[0]).stem) + ), extras=[ { 'name': 'index.yaml',