This commit is contained in:
j3d1 2026-09-06 01:51:19 +02:00
parent 240a508306
commit 7135fce421
18 changed files with 288 additions and 112 deletions

View file

@ -17,6 +17,7 @@ from nacl.signing import SigningKey
from nacl.encoding import HexEncoder
IMGS_DIR = os.path.join(os.path.dirname(__file__), 'imgs')
AVATARS_DIR = os.path.join(os.path.dirname(__file__), 'avatars')
IMAGE_ATTACHMENT_PROBABILITY = 0.9
@ -254,11 +255,11 @@ def load_shared_data():
return all_tags, all_categories, all_properties
def load_available_images():
"""Read every file in imgs/ and return a list of (data, mime_type) tuples."""
def _load_images_from_dir(directory):
"""Read every image file in the given directory and return a list of (data, mime_type) tuples."""
images = []
for filename in sorted(os.listdir(IMGS_DIR)):
filepath = os.path.join(IMGS_DIR, filename)
for filename in sorted(os.listdir(directory)):
filepath = os.path.join(directory, filename)
if not os.path.isfile(filepath):
continue
mime_type, _ = mimetypes.guess_type(filename)
@ -269,6 +270,16 @@ def load_available_images():
return images
def load_available_images():
"""Read every file in imgs/ and return a list of (data, mime_type) tuples."""
return _load_images_from_dir(IMGS_DIR)
def load_available_avatars():
"""Read every file in avatars/ and return a list of (data, mime_type) tuples."""
return _load_images_from_dir(AVATARS_DIR)
def generate_keypair():
"""Generate a signing key pair and return (hex_private_key, hex_public_key)."""
signing_key = SigningKey.generate()
@ -277,6 +288,18 @@ def generate_keypair():
return private_hex, public_hex
def load_or_generate_keypair(key_path):
"""Return (hex_private_key, hex_public_key), reusing the key at key_path if it already
exists instead of generating (and thereby invalidating) a new identity."""
if os.path.exists(key_path):
with open(key_path, 'r') as f:
private_hex = f.read().strip()
signing_key = SigningKey(private_hex.encode('utf-8'), encoder=HexEncoder)
public_hex = signing_key.verify_key.encode(encoder=HexEncoder).decode('utf-8')
return private_hex, public_hex
return generate_keypair()
def generate_locations_csv(count):
"""Generate a realistic locations.csv with varied storage locations and parent-child relationships."""
locations = random.sample(LOCATIONS_DATABASE, min(count, len(LOCATIONS_DATABASE)))
@ -442,6 +465,24 @@ def generate_inventory_csv(count, num_locations, location_id_map, item_range=Non
return output.getvalue().encode('utf-8'), files
def generate_profile_data(username, domain, first_name, last_name, avatar_data, avatar_mime_type):
"""Build profile.json content and the (arcname, data) for its picture, matching
`toolshed.offlinedata.profile_data()`/`profile_picture_files()`'s 'files/<hash><ext>' convention.
"""
extension = mimetypes.guess_extension(avatar_mime_type) or ''
arcname = f'files/{sha256(avatar_data).hexdigest()}{extension}'
profile = {
'username': username,
'domain': domain,
'email': f'{username}@{domain}',
'first_name': first_name,
'last_name': last_name,
'profile_picture': arcname,
}
return json.dumps(profile, indent=2).encode('utf-8'), arcname
def generate_sample_files():
"""Generate sample files for the 'files/' folder in the zip."""
files = {}
@ -458,7 +499,8 @@ def generate_sample_files():
def generate_export_zip(username, domain, friend_username, friend_domain, friend_public_key,
num_locations=10, num_items=150, item_range=None):
num_locations=10, num_items=150, item_range=None,
first_name='', last_name='', avatar=None):
"""Generate a zip file (bytes) like user_data() produces.
Args:
@ -470,6 +512,9 @@ def generate_export_zip(username, domain, friend_username, friend_domain, friend
num_locations: Number of locations to generate
num_items: Number of items to generate
item_range: Tuple of (start_idx, end_idx) to partition the ITEMS_DATABASE, or None for all items
first_name: First name for profile.json
last_name: Last name for profile.json
avatar: (data, mime_type) tuple for the profile picture, or None to skip profile.json
"""
num_locations = max(5, min(20, num_locations))
num_items = max(100, min(200, num_items))
@ -495,6 +540,13 @@ def generate_export_zip(username, domain, friend_username, friend_domain, friend
for arcname, content in sample_files.items():
zip_file.writestr(arcname, content)
if avatar:
avatar_data, avatar_mime_type = avatar
profile_json, avatar_arcname = generate_profile_data(
username, domain, first_name, last_name, avatar_data, avatar_mime_type)
zip_file.writestr('profile.json', profile_json)
zip_file.writestr(avatar_arcname, avatar_data)
return zip_buffer.getvalue()
@ -506,20 +558,30 @@ def main():
all_tags, all_categories, all_properties = load_shared_data()
print(f" ✓ Loaded {len(all_tags)} tags, {len(all_categories)} categories, and {len(all_properties)} properties")
print("\nGenerating keypairs...")
user_a_private, user_a_public = generate_keypair()
user_b_private, user_b_public = generate_keypair()
print("\nLoading/generating keypairs...")
user_a_key_path = os.path.join(script_dir, 'user-a.key')
user_b_key_path = os.path.join(script_dir, 'user-b.key')
user_a_existed = os.path.exists(user_a_key_path)
user_b_existed = os.path.exists(user_b_key_path)
user_a_private, user_a_public = load_or_generate_keypair(user_a_key_path)
user_b_private, user_b_public = load_or_generate_keypair(user_b_key_path)
print(f" user-a@a.localhost: {user_a_private[:16]}...{user_a_private[-16:]}")
print(f" user-b@b.localhost: {user_b_private[:16]}...{user_b_private[-16:]}")
print(f" test_a@a.localhost: {user_a_private[:16]}...{user_a_private[-16:]}")
print(f" test_b@b.localhost: {user_b_private[:16]}...{user_b_private[-16:]}")
print("\nWriting key files...")
with open(os.path.join(script_dir, 'user-a.key'), 'w') as f:
f.write(user_a_private)
with open(os.path.join(script_dir, 'user-b.key'), 'w') as f:
f.write(user_b_private)
print(" ✓ user-a.key")
print(" ✓ user-b.key")
if user_a_existed:
print(" ✓ user-a.key (reused existing)")
else:
with open(user_a_key_path, 'w') as f:
f.write(user_a_private)
print(" ✓ user-a.key (generated)")
if user_b_existed:
print(" ✓ user-b.key (reused existing)")
else:
with open(user_b_key_path, 'w') as f:
f.write(user_b_private)
print(" ✓ user-b.key (generated)")
print("\nGenerating realistic export zips with partitioned items...")
# Partition the ITEMS_DATABASE randomly
@ -531,13 +593,21 @@ def main():
print(f" User A items: {user_a_item_range[0]}-{user_a_item_range[1]} ({user_a_item_range[1] - user_a_item_range[0] + 1} unique items)")
print(f" User B items: {user_b_item_range[0]}-{user_b_item_range[1]} ({user_b_item_range[1] - user_b_item_range[0] + 1} unique items)")
available_avatars = load_available_avatars()
if len(available_avatars) >= 2:
user_a_avatar, user_b_avatar = random.sample(available_avatars, 2)
else:
user_a_avatar = user_b_avatar = available_avatars[0] if available_avatars else None
user_a_zip = generate_export_zip(
'user-a', 'a.localhost', 'user-b', 'b.localhost', user_b_public,
num_locations=12, num_items=175, item_range=user_a_item_range
'test_a', 'a.localhost', 'test_b', 'b.localhost', user_b_public,
num_locations=12, num_items=175, item_range=user_a_item_range,
first_name='Alice', last_name='Anderson', avatar=user_a_avatar
)
user_b_zip = generate_export_zip(
'user-b', 'b.localhost', 'user-a', 'a.localhost', user_a_public,
num_locations=8, num_items=140, item_range=user_b_item_range
'test_b', 'b.localhost', 'test_a', 'a.localhost', user_a_public,
num_locations=8, num_items=140, item_range=user_b_item_range,
first_name='Bob', last_name='Baker', avatar=user_b_avatar
)
with open(os.path.join(script_dir, 'user-a.zip'), 'wb') as f: