stash
BIN
testdata/avatars/avatar-2.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
testdata/avatars/avatar-3.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
testdata/avatars/avatar-4.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
testdata/avatars/avatar-5.png
vendored
Normal file
|
After Width: | Height: | Size: 1 MiB |
BIN
testdata/avatars/avatar-6.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
testdata/avatars/avatar.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
110
testdata/generate_testdata.py
vendored
|
|
@ -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:
|
||||
|
|
|
|||
BIN
testdata/imgs/unsplash-1.jpg
vendored
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
testdata/imgs/unsplash-2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 888 KiB |
BIN
testdata/imgs/unsplash-3.jpg
vendored
Normal file
|
After Width: | Height: | Size: 970 KiB |
2
testdata/user-a.key
vendored
|
|
@ -1 +1 @@
|
|||
982abc6680c1358c9d4d2436c96c00aebe36e9dd50fe184c12f1ac58fc7f8565
|
||||
53da21dc8bf556e45b906bc9f1cd22688b29efb2e575c8fb8353199a93553d4b
|
||||
BIN
testdata/user-a.zip
vendored
2
testdata/user-b.key
vendored
|
|
@ -1 +1 @@
|
|||
2e81e04093250c647cb2c364e39017be5aeac0303e138d6ded2e2cc7c3cdcdde
|
||||
8ed48f39bd054d88124d7b2ae4529a3da692b131194bd26da2bf5ede612baea9
|
||||