Bombsquad-Ballistica-Modded.../dist/ba_data/python/bacommon/transfer.py

104 lines
3.4 KiB
Python
Raw Normal View History

2022-06-30 00:31:52 +05:30
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to transferring files/data."""
from __future__ import annotations
import os
2022-07-16 17:59:14 +05:30
from pathlib import Path
2022-06-30 00:31:52 +05:30
from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated
from efro.dataclassio import ioprepped, IOAttrs
if TYPE_CHECKING:
2022-07-16 17:59:14 +05:30
pass
2022-06-30 00:31:52 +05:30
@ioprepped
@dataclass
class DirectoryManifestFile:
2024-03-10 15:37:50 +05:30
"""Describes a file in a manifest."""
2024-03-10 15:37:50 +05:30
hash_sha256: Annotated[str, IOAttrs('h')]
size: Annotated[int, IOAttrs('s')]
2022-06-30 00:31:52 +05:30
@ioprepped
@dataclass
class DirectoryManifest:
"""Contains a summary of files in a directory."""
2022-06-30 00:31:52 +05:30
files: Annotated[dict[str, DirectoryManifestFile], IOAttrs('f')]
2024-01-27 21:25:16 +05:30
# _empty_hash: str | None = None
2022-06-30 00:31:52 +05:30
@classmethod
def create_from_disk(cls, path: Path) -> DirectoryManifest:
"""Create a manifest from a directory on disk."""
import hashlib
from concurrent.futures import ThreadPoolExecutor
pathstr = str(path)
paths: list[str] = []
if path.is_dir():
2022-07-16 17:59:14 +05:30
# Build the full list of relative paths.
2022-06-30 00:31:52 +05:30
for basename, _dirnames, filenames in os.walk(path):
for filename in filenames:
fullname = os.path.join(basename, filename)
assert fullname.startswith(pathstr)
2022-07-16 17:59:14 +05:30
# Make sure we end up with forward slashes no matter
# what the os.* stuff above here was using.
paths.append(Path(fullname[len(pathstr) + 1 :]).as_posix())
2022-06-30 00:31:52 +05:30
elif path.exists():
# Just return a single file entry if path is not a dir.
2022-07-16 17:59:14 +05:30
paths.append(path.as_posix())
2022-06-30 00:31:52 +05:30
def _get_file_info(filepath: str) -> tuple[str, DirectoryManifestFile]:
sha = hashlib.sha256()
fullfilepath = os.path.join(pathstr, filepath)
if not os.path.isfile(fullfilepath):
2023-08-13 17:21:49 +05:30
raise RuntimeError(f'File not found: "{fullfilepath}".')
2022-06-30 00:31:52 +05:30
with open(fullfilepath, 'rb') as infile:
filebytes = infile.read()
filesize = len(filebytes)
sha.update(filebytes)
return (
filepath,
DirectoryManifestFile(
2024-03-10 15:37:50 +05:30
hash_sha256=sha.hexdigest(), size=filesize
),
)
2022-06-30 00:31:52 +05:30
# Now use all procs to hash the files efficiently.
cpus = os.cpu_count()
if cpus is None:
cpus = 4
with ThreadPoolExecutor(max_workers=cpus) as executor:
return cls(files=dict(executor.map(_get_file_info, paths)))
2022-07-16 17:59:14 +05:30
def validate(self) -> None:
"""Log any odd data in the manifest; for debugging."""
import logging
2022-07-16 17:59:14 +05:30
for fpath, _fentry in self.files.items():
# We want to be dealing in only forward slashes; make sure
# that's the case (wondering if we'll ever see backslashes
# for escape purposes).
if '\\' in fpath:
logging.exception(
"Found unusual path in manifest: '%s'.", fpath
)
2022-07-16 17:59:14 +05:30
break # 1 error is enough for now.
2024-01-27 21:25:16 +05:30
# @classmethod
# def get_empty_hash(cls) -> str:
# """Return the hash for an empty file."""
# if cls._empty_hash is None:
# import hashlib
2024-01-27 21:25:16 +05:30
# sha = hashlib.sha256()
# cls._empty_hash = sha.hexdigest()
# return cls._empty_hash