66
77import logging
88import os
9+ import shutil
910import subprocess
11+ import zipfile
12+ from datetime import datetime
13+ from pathlib import Path
14+ from tempfile import mkdtemp
1015from urllib .parse import urlparse
1116
1217from django .conf import settings
1318from django .contrib .auth .models import User # lint-amnesty, pylint: disable=imported-auth-user
1419from django .utils import timezone
20+ from django .utils .text import slugify
1521from django .utils .translation import gettext_lazy as _
22+ from opaque_keys .edx .locator import LibraryLocator , LibraryLocatorV2
1623
1724from xmodule .contentstore .django import contentstore
1825from 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
2128log = 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