Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions openedx_learning/apps/authoring/backup_restore/zipper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
PublishableEntity,
PublishableEntityVersion,
)
from openedx_learning.lib.dump_naming import hash_slugify_filename

TOML_PACKAGE_NAME = "package.toml"

Expand Down Expand Up @@ -73,7 +74,8 @@ def create_zip(self, path: str) -> None:
entity_toml_content: str = toml_publishable_entity(entity)

if hasattr(entity, 'container'):
entity_toml_filename = f"{entity.key}.toml"
entity_slugify_hash = hash_slugify_filename(entity.key)
entity_toml_filename = f"{entity_slugify_hash}.toml"
entity_toml_path = entities_folder / entity_toml_filename
zipf.writestr(str(entity_toml_path), entity_toml_content)

Expand All @@ -87,7 +89,7 @@ def create_zip(self, path: str) -> None:
# component_versions/
# v1/
# static/

entity_slugify_hash = hash_slugify_filename(entity.component.local_key)
component_namespace_folder = entities_folder / entity.component.component_type.namespace
# Example of component namespace is: "xblock.v1"
self.create_folder(component_namespace_folder, zipf)
Expand All @@ -96,12 +98,12 @@ def create_zip(self, path: str) -> None:
# Example of component type is: "html"
self.create_folder(component_type_folder, zipf)

component_id_folder = component_type_folder / entity.component.local_key # entity.key
component_id_folder = component_type_folder / entity_slugify_hash
# Example of component id is: "i-dont-like-the-sidebar-aa1645ade4a7"
self.create_folder(component_id_folder, zipf)

# Add the entity TOML file inside the component type folder as well
component_entity_toml_path = component_type_folder / f"{entity.component.local_key}.toml"
component_entity_toml_path = component_type_folder / f"{entity_slugify_hash}.toml"
zipf.writestr(str(component_entity_toml_path), entity_toml_content)

# Add component version folder into the component id folder
Expand Down
39 changes: 39 additions & 0 deletions openedx_learning/lib/dump_naming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
This module provides utilities for generating safe and consistent filenames for
dumping learning package content.

Examples
print(hash_slugify_filename("My:Component")) # → my_component_a12f4c.toml
print(hash_slugify_filename("Math/Algebra+V2")) # → math_algebra+v2_8d31a.toml
print(hash_slugify_filename("User@Library#1")) # → user@library#1_c9a0f.toml
"""
Comment thread
dwong2708 marked this conversation as resolved.
Outdated
import hashlib
import re
from typing import Optional

# Windows-invalid filename characters
INVALID_CHARS = r'<>:"/\\|?*'


def safe_slugify(value: str, separator: str = "_") -> str:
"""
Make a filesystem-safe slug:
- Lowercase
- Replace only invalid filename characters with separator
- Collapse multiple separators
- Strip leading/trailing separators
"""
value = value.lower()
value = re.sub(f"[{re.escape(INVALID_CHARS)}]", separator, value)
value = re.sub(f"{separator}+", separator, value) # collapse duplicates
return value.strip(separator)
Comment thread
dwong2708 marked this conversation as resolved.
Outdated


def hash_slugify_filename(identifier: str, ext: Optional[str] = "") -> str:
Comment thread
dwong2708 marked this conversation as resolved.
Outdated
"""
Hybrid filename = slugified identifier + short hash.
Keeps most special characters intact except filesystem-invalid ones.
Comment thread
dwong2708 marked this conversation as resolved.
Outdated
"""
slug = safe_slugify(identifier)
short_hash = hashlib.sha1(identifier.encode("utf-8")).hexdigest()[:5]
Comment thread
dwong2708 marked this conversation as resolved.
Outdated
return f"{slug}_{short_hash}{ext}"
24 changes: 12 additions & 12 deletions tests/openedx_learning/apps/authoring/backup_restore/test_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,17 @@ def check_zip_file_structure(self, zip_path: Path):
"collections/",
"entities/xblock.v1/",
"entities/xblock.v1/html/",
"entities/xblock.v1/html/my_draft_example.toml",
"entities/xblock.v1/html/my_draft_example/",
"entities/xblock.v1/html/my_draft_example/component_versions/",
"entities/xblock.v1/html/my_draft_example/component_versions/v2/",
"entities/xblock.v1/html/my_draft_example/component_versions/v2/static/",
"entities/xblock.v1/html/my_draft_example_ac6d7.toml",
"entities/xblock.v1/html/my_draft_example_ac6d7/",
"entities/xblock.v1/html/my_draft_example_ac6d7/component_versions/",
"entities/xblock.v1/html/my_draft_example_ac6d7/component_versions/v2/",
"entities/xblock.v1/html/my_draft_example_ac6d7/component_versions/v2/static/",
"entities/xblock.v1/problem/",
"entities/xblock.v1/problem/my_published_example/",
"entities/xblock.v1/problem/my_published_example.toml",
"entities/xblock.v1/problem/my_published_example/component_versions/",
"entities/xblock.v1/problem/my_published_example/component_versions/v1/",
"entities/xblock.v1/problem/my_published_example/component_versions/v1/static/",
"entities/xblock.v1/problem/my_published_example_fff05/",
"entities/xblock.v1/problem/my_published_example_fff05.toml",
"entities/xblock.v1/problem/my_published_example_fff05/component_versions/",
"entities/xblock.v1/problem/my_published_example_fff05/component_versions/v1/",
"entities/xblock.v1/problem/my_published_example_fff05/component_versions/v1/static/",
]

# Check that all expected paths are present
Expand Down Expand Up @@ -166,7 +166,7 @@ def test_lp_dump_command(self):

# Check the content of the entity TOML files
expected_files = {
"entities/xblock.v1/problem/my_published_example.toml": [
"entities/xblock.v1/problem/my_published_example_fff05.toml": [
'[entity]',
f'uuid = "{self.published_component.uuid}"',
'can_stand_alone = true',
Expand All @@ -175,7 +175,7 @@ def test_lp_dump_command(self):
'[entity.published]',
'version_num = 1',
],
"entities/xblock.v1/html/my_draft_example.toml": [
"entities/xblock.v1/html/my_draft_example_ac6d7.toml": [
'[entity]',
f'uuid = "{self.draft_component.uuid}"',
'can_stand_alone = true',
Expand Down