Skip to content

Commit 24001ee

Browse files
feat: support added to export content libraries to git
1 parent 110ec0c commit 24001ee

File tree

1 file changed

+109
-13
lines changed

1 file changed

+109
-13
lines changed

cms/djangoapps/contentstore/git_export_utils.py

Lines changed: 109 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@
66

77
import logging
88
import os
9+
import shutil
910
import subprocess
11+
import zipfile
12+
from datetime import datetime
13+
from pathlib import Path
14+
from tempfile import mkdtemp
1015
from urllib.parse import urlparse
1116

1217
from django.conf import settings
1318
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
1419
from django.utils import timezone
20+
from django.utils.text import slugify
1521
from django.utils.translation import gettext_lazy as _
22+
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
1623

1724
from xmodule.contentstore.django import contentstore
1825
from xmodule.modulestore.django import modulestore
19-
from xmodule.modulestore.xml_exporter import export_course_to_xml
26+
from xmodule.modulestore.xml_exporter import export_course_to_xml, export_library_to_xml
2027

2128
log = logging.getLogger(__name__)
2229

@@ -66,10 +73,89 @@ def cmd_log(cmd, cwd):
6673
return output
6774

6875

69-
def export_to_git(course_id, repo, user='', rdir=None):
70-
"""Export a course to git."""
76+
def export_library_v2_to_zip(library_key, root_dir, library_dir, user=None):
77+
"""
78+
Export a v2 library using the backup API.
79+
80+
V2 libraries are stored in Learning Core and use a zip-based backup mechanism.
81+
This function creates a zip backup and extracts it to the specified directory.
82+
83+
Args:
84+
library_key: LibraryLocatorV2 for the library to export
85+
root_dir: Root directory where library_dir will be created
86+
library_dir: Directory name for the exported library content
87+
user: User object for the backup API (optional)
88+
89+
Raises:
90+
Exception: If backup creation or extraction fails
91+
"""
92+
from openedx_content.api import create_zip_file as create_lib_zip_file
93+
94+
# Get user object for backup API
95+
user_obj = User.objects.filter(username=user).first()
96+
# Create temporary zip backup
97+
temp_dir = Path(mkdtemp())
98+
sanitized_lib_key = str(library_key).replace(":", "-")
99+
sanitized_lib_key = slugify(sanitized_lib_key, allow_unicode=True)
100+
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
101+
zip_filename = f'{sanitized_lib_key}-{timestamp}.zip'
102+
zip_path = os.path.join(temp_dir, zip_filename)
103+
104+
try:
105+
origin_server = getattr(settings, 'CMS_BASE', None)
106+
create_lib_zip_file(
107+
lp_key=str(library_key),
108+
path=zip_path,
109+
user=user_obj,
110+
origin_server=origin_server
111+
)
112+
113+
# Target directory for extraction
114+
target_dir = os.path.join(root_dir, library_dir)
115+
116+
# Create target directory if it doesn't exist
117+
os.makedirs(target_dir, exist_ok=True)
118+
119+
# Extract zip contents (will overwrite existing files)
120+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
121+
zip_ref.extractall(target_dir)
122+
123+
log.info('Extracted library v2 backup to %s', target_dir)
124+
125+
finally:
126+
# Cleanup temporary files
127+
if temp_dir.exists():
128+
shutil.rmtree(temp_dir)
129+
130+
131+
def export_to_git(content_key, repo, user='', rdir=None):
132+
"""
133+
Export a course or library to git.
134+
135+
Args:
136+
content_key: CourseKey or LibraryLocator for the content to export
137+
repo (str): Git repository URL
138+
user (str): Optional username for git commit identity
139+
rdir (str): Optional custom directory name for the repository
140+
141+
Raises:
142+
GitExportError: For various git operation failures
143+
"""
71144
# pylint: disable=too-many-statements
72145

146+
# Detect content type and select appropriate export function
147+
is_library_v2 = isinstance(content_key, LibraryLocatorV2)
148+
if is_library_v2:
149+
# V2 libraries use backup API with zip extraction
150+
export_xml_func = export_library_v2_to_zip
151+
content_type_label = "library"
152+
elif isinstance(content_key, LibraryLocator):
153+
export_xml_func = export_library_to_xml
154+
content_type_label = "library"
155+
else:
156+
export_xml_func = export_course_to_xml
157+
content_type_label = "course"
158+
73159
if not GIT_REPO_EXPORT_DIR:
74160
raise GitExportError(GitExportError.NO_EXPORT_DIR)
75161

@@ -128,15 +214,20 @@ def export_to_git(course_id, repo, user='', rdir=None):
128214
log.exception('Failed to pull git repository: %r', ex.output)
129215
raise GitExportError(GitExportError.CANNOT_PULL) from ex
130216

131-
# export course as xml before commiting and pushing
217+
# export content as xml (or zip for v2 libraries) before commiting and pushing
132218
root_dir = os.path.dirname(rdirp)
133-
course_dir = os.path.basename(rdirp).rsplit('.git', 1)[0]
219+
content_dir = os.path.basename(rdirp).rsplit('.git', 1)[0]
220+
134221
try:
135-
export_course_to_xml(modulestore(), contentstore(), course_id,
136-
root_dir, course_dir)
137-
except (OSError, AttributeError):
138-
log.exception('Failed export to xml')
139-
raise GitExportError(GitExportError.XML_EXPORT_FAIL) # lint-amnesty, pylint: disable=raise-missing-from
222+
if is_library_v2:
223+
export_xml_func(content_key, root_dir, content_dir, user)
224+
else:
225+
# V1 libraries and courses: use XML export (no user parameter)
226+
export_xml_func(modulestore(), contentstore(), content_key,
227+
root_dir, content_dir)
228+
except (OSError, AttributeError) as ex:
229+
log.exception('Failed to export %s', content_type_label)
230+
raise GitExportError(GitExportError.XML_EXPORT_FAIL) from ex
140231

141232
# Get current branch if not already set
142233
if not branch:
@@ -160,9 +251,7 @@ def export_to_git(course_id, repo, user='', rdir=None):
160251
ident = GIT_EXPORT_DEFAULT_IDENT
161252
time_stamp = timezone.now()
162253
cwd = os.path.abspath(rdirp)
163-
commit_msg = "Export from Studio at {time_stamp}".format(
164-
time_stamp=time_stamp,
165-
)
254+
commit_msg = f"Export {content_type_label} from Studio at {time_stamp}"
166255
try:
167256
cmd_log(['git', 'config', 'user.email', ident['email']], cwd)
168257
cmd_log(['git', 'config', 'user.name', ident['name']], cwd)
@@ -180,3 +269,10 @@ def export_to_git(course_id, repo, user='', rdir=None):
180269
except subprocess.CalledProcessError as ex:
181270
log.exception('Error running git push command: %r', ex.output)
182271
raise GitExportError(GitExportError.CANNOT_PUSH) from ex
272+
273+
log.info(
274+
'%s %s exported to git repository %s successfully',
275+
content_type_label.capitalize(),
276+
content_key,
277+
repo,
278+
)

0 commit comments

Comments
 (0)