From f3c227e1784691a5c9b91ea8380a43aba96e7f52 Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Wed, 18 Sep 2019 12:33:54 +0200
Subject: [PATCH] OO-4255: streams the course export to the browser without
 copy of course

---
 src/main/java/org/olat/core/util/ZipUtil.java |  38 ++
 .../java/org/olat/course/CourseFactory.java   |  32 --
 src/main/java/org/olat/course/ICourse.java    |  11 -
 .../org/olat/course/PersistingCourseImpl.java | 152 +------
 .../export/CourseExportMediaResource.java     | 427 ++++++++++++++++++
 .../groupsandrights/CourseGroupManager.java   |   2 +-
 .../PersistingCourseGroupManager.java         |   4 +-
 .../preview/PreviewCourseGroupManager.java    |   2 +-
 .../handler/BinderTemplateMediaResource.java  |   2 +-
 .../modules/reminder/ReminderService.java     |   3 +-
 .../reminder/manager/ReminderServiceImpl.java |  23 +-
 .../RepositoryEntryImportExport.java          |  98 ++--
 .../repository/handlers/CourseHandler.java    |  11 +-
 13 files changed, 549 insertions(+), 256 deletions(-)
 create mode 100644 src/main/java/org/olat/course/export/CourseExportMediaResource.java

diff --git a/src/main/java/org/olat/core/util/ZipUtil.java b/src/main/java/org/olat/core/util/ZipUtil.java
index e09036d903d..68ee5d8b049 100644
--- a/src/main/java/org/olat/core/util/ZipUtil.java
+++ b/src/main/java/org/olat/core/util/ZipUtil.java
@@ -835,6 +835,44 @@ public class ZipUtil {
 		}
 	}
 	
+	/**
+	 * Add a directory to a zip stream. The files path are relative to the
+	 * specified directory. The name of the directory is not part of
+	 * the path of its files.
+	 * 
+	 * @param path The path
+	 * @param directory The directory to zip
+	 * @param exportStream The stream
+	 */
+	public static void addPathToZip(final String path, final Path directory, final ZipOutputStream exportStream) {
+		try {
+			Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
+				@Override
+				public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+					if(!attrs.isDirectory()) {
+						Path relativeFile = directory.relativize(file);
+						String name = relativeFile.toString();
+						if(StringHelper.containsNonWhitespace(path)) {
+							name = path + "/" + name;
+						}
+						exportStream.putNextEntry(new ZipEntry(name));
+						
+						try(InputStream in=Files.newInputStream(file)) {
+							FileUtils.cpio(in, exportStream, "");
+						} catch (Exception e) {
+							log.error("", e);
+						}
+						
+						exportStream.closeEntry();
+					}
+					return FileVisitResult.CONTINUE;
+				}
+			});
+		} catch (IOException e) {
+			log.error("", e);
+		}
+	}
+	
 	/**
 	 * Zip all files under a certain root directory. (with compression)
 	 * 
diff --git a/src/main/java/org/olat/course/CourseFactory.java b/src/main/java/org/olat/course/CourseFactory.java
index fa7a5685a51..791531add00 100644
--- a/src/main/java/org/olat/course/CourseFactory.java
+++ b/src/main/java/org/olat/course/CourseFactory.java
@@ -32,11 +32,9 @@ import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.zip.ZipOutputStream;
@@ -73,7 +71,6 @@ import org.olat.core.id.context.ContextEntry;
 import org.olat.core.logging.AssertException;
 import org.olat.core.logging.OLATRuntimeException;
 import org.olat.core.logging.Tracing;
-import org.olat.core.util.CodeHelper;
 import org.olat.core.util.ExportUtil;
 import org.olat.core.util.FileUtils;
 import org.olat.core.util.Formatter;
@@ -81,7 +78,6 @@ import org.olat.core.util.ObjectCloner;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.UserSession;
 import org.olat.core.util.Util;
-import org.olat.core.util.WebappHelper;
 import org.olat.core.util.ZipUtil;
 import org.olat.core.util.cache.CacheWrapper;
 import org.olat.core.util.coordinate.CoordinatorManager;
@@ -504,34 +500,6 @@ public class CourseFactory {
 		return targetRes;
 	}
 
-	/**
-	 * Exports an entire course to a zip file.
-	 *
-	 * @param sourceRes
-	 * @param fTargetZIP
-	 * @return true if successfully exported, false otherwise.
-	 */
-	public static void exportCourseToZIP(OLATResourceable sourceRes, File fTargetZIP, boolean runtimeDatas) {
-		PersistingCourseImpl sourceCourse = (PersistingCourseImpl) loadCourse(sourceRes);
-
-		// add files to ZIP
-		File fExportDir = new File(WebappHelper.getTmpDir(), CodeHelper.getUniqueID());
-		fExportDir.mkdirs();
-		log.info("Export folder: " + fExportDir);
-		synchronized (sourceCourse) { //o_clusterNOK - cannot be solved with doInSync since could take too long (leads to error: "Lock wait timeout exceeded")
-			OLATResource courseResource = sourceCourse.getCourseEnvironment().getCourseGroupManager().getCourseResource();
-			sourceCourse.exportToFilesystem(courseResource, fExportDir, runtimeDatas);
-			Set<String> fileSet = new HashSet<>();
-			String[] files = fExportDir.list();
-			for (int i = 0; i < files.length; i++) {
-				fileSet.add(files[i]);
-			}
-			ZipUtil.zip(fileSet, fExportDir, fTargetZIP, false);
-			log.info("Delete export folder: " + fExportDir);
-			FileUtils.deleteDirsAndFiles(fExportDir, true, true);
-		}
-	}
-
 	/**
 	 * Import a course from a ZIP file.
 	 *
diff --git a/src/main/java/org/olat/course/ICourse.java b/src/main/java/org/olat/course/ICourse.java
index 1c6a875588b..10e1ee71fd5 100644
--- a/src/main/java/org/olat/course/ICourse.java
+++ b/src/main/java/org/olat/course/ICourse.java
@@ -36,7 +36,6 @@ import org.olat.course.export.CourseEnvironmentMapper;
 import org.olat.course.folder.CourseContainerOptions;
 import org.olat.course.run.environment.CourseEnvironment;
 import org.olat.course.tree.CourseEditorTreeModel;
-import org.olat.resource.OLATResource;
 
 /**
  * Description:<BR/>
@@ -62,16 +61,6 @@ public interface ICourse extends OLATResourceable {
 	 * @return The course editor tree model for this course
 	 */
 	public CourseEditorTreeModel getEditorTreeModel();
-		
-	/**
-	 * Export course to file system.
-	 * @param originalCourseResource The original resource
-	 * @param exportDirectory The directory to export files to.
-	 * @param runtimeDatas Export with runtime datas (true add archives of the groups...)
-	 * @param backwardsCompatible Export in a format compatible with older OpenOLAT version
-	 * @param foldersToCleanup Can add there folders which need to be clean up after the export
-	 */
-	public void exportToFilesystem(OLATResource originalCourseResource, File exportDirectory, boolean runtimeDatas);
 	
 	public void postCopy(CourseEnvironmentMapper envMapper, ICourse sourceCourse);
 	
diff --git a/src/main/java/org/olat/course/PersistingCourseImpl.java b/src/main/java/org/olat/course/PersistingCourseImpl.java
index 0be14b62e57..bcf9696b9e6 100644
--- a/src/main/java/org/olat/course/PersistingCourseImpl.java
+++ b/src/main/java/org/olat/course/PersistingCourseImpl.java
@@ -29,20 +29,16 @@ import java.io.File;
 import java.io.InputStream;
 import java.io.Serializable;
 
+import org.apache.logging.log4j.Logger;
 import org.olat.admin.quota.QuotaConstants;
 import org.olat.core.CoreSpringFactory;
-import org.olat.core.commons.persistence.DBFactory;
 import org.olat.core.commons.services.vfs.VFSRepositoryService;
 import org.olat.core.id.IdentityEnvironment;
 import org.olat.core.id.OLATResourceable;
 import org.olat.core.logging.AssertException;
 import org.olat.core.logging.OLATRuntimeException;
-import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
-import org.olat.core.util.FileUtils;
-import org.olat.core.util.ZipUtil;
 import org.olat.core.util.nodes.INode;
-import org.olat.core.util.tree.TreeVisitor;
 import org.olat.core.util.tree.Visitor;
 import org.olat.core.util.vfs.LocalFolderImpl;
 import org.olat.core.util.vfs.VFSContainer;
@@ -63,11 +59,7 @@ import org.olat.course.run.environment.CourseEnvironment;
 import org.olat.course.run.environment.CourseEnvironmentImpl;
 import org.olat.course.tree.CourseEditorTreeModel;
 import org.olat.course.tree.CourseEditorTreeNode;
-import org.olat.modules.glossary.GlossaryManager;
-import org.olat.modules.reminder.ReminderService;
-import org.olat.modules.sharedfolder.SharedFolderManager;
 import org.olat.repository.RepositoryEntry;
-import org.olat.repository.RepositoryEntryImportExport;
 import org.olat.repository.RepositoryManager;
 import org.olat.resource.OLATResource;
 
@@ -90,9 +82,9 @@ public class PersistingCourseImpl implements ICourse, OLATResourceable, Serializ
 
 	public static final String COURSE_ROOT_DIR_NAME = "course";
 	
-	private static final String EDITORTREEMODEL_XML = "editortreemodel.xml";
-	private static final String RUNSTRUCTURE_XML = "runstructure.xml";
-	private static final String ORES_TYPE_NAME = CourseModule.getCourseTypeName();
+	public static final String EDITORTREEMODEL_XML = "editortreemodel.xml";
+	public static final String RUNSTRUCTURE_XML = "runstructure.xml";
+	public static final String ORES_TYPE_NAME = CourseModule.getCourseTypeName();
 	public static final String COURSEFOLDER = "coursefolder";
 
 	private Long resourceableId;
@@ -260,7 +252,7 @@ public class PersistingCourseImpl implements ICourse, OLATResourceable, Serializ
 	/**
 	 * @return The directory "coursefolder" or storage folder of the course
 	 */
-	protected File getIsolatedCourseBaseFolder() {
+	public File getIsolatedCourseBaseFolder() {
 		// create local course folder
 		return VFSManager.olatRootDirectory(courseRootContainer.getRelPath() + File.separator + COURSEFOLDER);
 	}
@@ -268,7 +260,7 @@ public class PersistingCourseImpl implements ICourse, OLATResourceable, Serializ
 	/**
 	 * @return The directory "coursefolder" or storage folder of the course
 	 */
-	protected VFSContainer getIsolatedCourseBaseContainer() {
+	public VFSContainer getIsolatedCourseBaseContainer() {
 		// create local course folder
 		return VFSManager.olatRootContainer(courseRootContainer.getRelPath() + File.separator + COURSEFOLDER, null);
 	}
@@ -288,107 +280,6 @@ public class PersistingCourseImpl implements ICourse, OLATResourceable, Serializ
 		writeObject(EDITORTREEMODEL_XML, getEditorTreeModel());
 		log.debug("saveEditorTreeModel");
 	}
-
-	/**
-	 * @see org.olat.course.ICourse#exportToFilesystem(java.io.File)
-	 * <p>
-	 * See OLAT-5368: Course Export can take longer than say 2min.
-	 * <p>
-	 */
-	@Override
-	public void exportToFilesystem(OLATResource originalCourseResource, File exportDirectory, boolean runtimeDatas) {
-		long s = System.currentTimeMillis();
-		log.info("exportToFilesystem: exporting course "+this+" to "+exportDirectory+"...");
-		File fCourseBase = getCourseBaseContainer().getBasefile();
-		//make the folder structure
-		File fExportedDataDir = new File(exportDirectory, EXPORTED_DATA_FOLDERNAME);
-		fExportedDataDir.mkdirs();
-
-		//export course config
-		FileUtils.copyFileToDir(new File(fCourseBase, CourseConfigManager.COURSECONFIG_XML), exportDirectory, "course export courseconfig");
-		
-		//export business groups
-		CourseEnvironmentMapper envMapper = getCourseEnvironment().getCourseGroupManager().getBusinessGroupEnvironment();
-
-		getCourseEnvironment().getCourseGroupManager().exportCourseBusinessGroups(fExportedDataDir, envMapper, runtimeDatas);
-		// export editor structure
-		FileUtils.copyFileToDir(new File(fCourseBase, EDITORTREEMODEL_XML), exportDirectory, "course export exitortreemodel");
-		// export run structure
-		FileUtils.copyFileToDir(new File(fCourseBase, RUNSTRUCTURE_XML), exportDirectory, "course export runstructure");
-
-		// export layout and media folder
-		FileUtils.copyDirToDir(new File(fCourseBase, "layout"), exportDirectory, "course export layout folder");
-		FileUtils.copyDirToDir(new File(fCourseBase, "media"), exportDirectory, "course export media folder");
-		// export course folder
-		File fExportedCoursefolderZip = new File(exportDirectory, "oocoursefolder.zip");
-		File courseFolder = getIsolatedCourseBaseFolder();
-		ZipUtil.zipAll(courseFolder, fExportedCoursefolderZip);
-
-		// export any node data
-		log.info("exportToFilesystem: exporting course "+this+": exporting all nodes...");
-		Visitor visitor = new NodeExportVisitor(fExportedDataDir, this);
-		TreeVisitor tv = new TreeVisitor(visitor, getEditorTreeModel().getRootNode(), true);
-		tv.visitAll();
-		log.info("exportToFilesystem: exporting course "+this+": exporting all nodes...done.");
-		
-		// Do intermediate commit to avoid transaction timeout
-		DBFactory.getInstance().intermediateCommit();
-
-		// export shared folder
-		CourseConfig config = getCourseConfig();
-		if (config.hasCustomSharedFolder()) {
-			log.info("exportToFilesystem: exporting course "+this+": shared folder...");
-			if (!SharedFolderManager.getInstance().exportSharedFolder(
-					config.getSharedFolderSoftkey(), fExportedDataDir)) {
-				// export failed, delete reference to shared folder in the course config
-				log.info("exportToFilesystem: exporting course "+this+": export of shared folder failed.");
-				config.setSharedFolderSoftkey(CourseConfig.VALUE_EMPTY_SHAREDFOLDER_SOFTKEY);
-				CoreSpringFactory.getImpl(CourseConfigManager.class).saveConfigTo(this, config);
-			}
-			log.info("exportToFilesystem: exporting course "+this+": shared folder...done.");
-		}
-		
-		// Do intermediate commit to avoid transaction timeout
-		DBFactory.getInstance().intermediateCommit();
-
-		// export glossary
-		if (config.hasGlossary()) {
-			log.info("exportToFilesystem: exporting course "+this+": glossary...");
-			final GlossaryManager glossaryManager = CoreSpringFactory.getImpl(GlossaryManager.class);
-			final CourseConfigManager courseConfigManager = CoreSpringFactory.getImpl(CourseConfigManager.class);
-			if (!glossaryManager.exportGlossary(
-					config.getGlossarySoftKey(), fExportedDataDir)) {
-				// export failed, delete reference to glossary in the course config
-				log.info("exportToFilesystem: exporting course "+this+": export of glossary failed.");
-				config.setGlossarySoftKey(null);
-				courseConfigManager.saveConfigTo(this, config);
-			}
-			log.info("exportToFilesystem: exporting course "+this+": glossary...done.");
-		}
-		
-		// Do intermediate commit to avoid transaction timeout
-		DBFactory.getInstance().intermediateCommit();
-
-		log.info("exportToFilesystem: exporting course "+this+": configuration and repo data...");
-		// export configuration file
-		FileUtils.copyFileToDir(new File(fCourseBase, CourseConfigManager.COURSECONFIG_XML), exportDirectory, "course export configuration and repo info");
-		
-		// export repo metadata
-		RepositoryManager rm = RepositoryManager.getInstance();
-		RepositoryEntry myRE = rm.lookupRepositoryEntry(this, true);
-		RepositoryEntryImportExport importExport = new RepositoryEntryImportExport(myRE, fExportedDataDir);
-		importExport.exportDoExportProperties();
-		
-		// Do intermediate commit to avoid transaction timeout
-		DBFactory.getInstance().intermediateCommit();
-		
-		//export reminders
-		CoreSpringFactory.getImpl(ReminderService.class)
-			.exportReminders(myRE, fExportedDataDir);
-
-		log.info("exportToFilesystem: exporting course "+this+" to "+exportDirectory+" done.");
-		log.info("finished export course '"+getCourseTitle()+"' in t="+Long.toString(System.currentTimeMillis()-s));
-	}
 	
 	@Override
 	public void postCopy(CourseEnvironmentMapper envMapper, ICourse sourceCourse) {
@@ -614,34 +505,3 @@ class NodePostCopyVisitor implements Visitor {
 		}
 	}
 }
-
-class NodeExportVisitor implements Visitor {
-
-	private File exportDirectory;
-	private ICourse course;
-
-	/**
-	 * Constructor of the node deletion visitor
-	 * 
-	 * @param exportDirectory
-	 * @param course
-	 */
-	public NodeExportVisitor(File exportDirectory, ICourse course) {
-		this.exportDirectory = exportDirectory;
-		this.course = course;
-	}
-
-	/**
-	 * Visitor pattern to delete the course nodes
-	 * 
-	 * @see org.olat.core.util.tree.Visitor#visit(org.olat.core.util.nodes.INode)
-	 */
-	@Override
-	public void visit(INode node) {
-		CourseEditorTreeNode cNode = (CourseEditorTreeNode) node;
-		cNode.getCourseNode().exportNode(exportDirectory, course);
-		// Do frequent intermediate commits to avoid transaction timeout
-		DBFactory.getInstance().intermediateCommit();
-	}
-
-}
diff --git a/src/main/java/org/olat/course/export/CourseExportMediaResource.java b/src/main/java/org/olat/course/export/CourseExportMediaResource.java
new file mode 100644
index 00000000000..88b13d58d4c
--- /dev/null
+++ b/src/main/java/org/olat/course/export/CourseExportMediaResource.java
@@ -0,0 +1,427 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.export;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.logging.log4j.Logger;
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.commons.persistence.DBFactory;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.gui.media.ServletUtil;
+import org.olat.core.id.OLATResourceable;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.CodeHelper;
+import org.olat.core.util.FileUtils;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.WebappHelper;
+import org.olat.core.util.ZipUtil;
+import org.olat.core.util.io.ShieldOutputStream;
+import org.olat.core.util.io.SystemFileFilter;
+import org.olat.core.util.nodes.INode;
+import org.olat.core.util.tree.TreeVisitor;
+import org.olat.core.util.vfs.LocalFolderImpl;
+import org.olat.core.util.vfs.VFSContainer;
+import org.olat.core.util.vfs.VFSManager;
+import org.olat.course.CourseFactory;
+import org.olat.course.ICourse;
+import org.olat.course.PersistingCourseImpl;
+import org.olat.course.config.CourseConfig;
+import org.olat.course.config.CourseConfigManager;
+import org.olat.course.nodes.BCCourseNode;
+import org.olat.course.nodes.CPCourseNode;
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.nodes.ScormCourseNode;
+import org.olat.course.nodes.VideoCourseNode;
+import org.olat.course.nodes.cp.CPEditController;
+import org.olat.course.nodes.video.VideoEditController;
+import org.olat.course.tree.CourseEditorTreeNode;
+import org.olat.modules.glossary.GlossaryManager;
+import org.olat.modules.reminder.ReminderService;
+import org.olat.modules.sharedfolder.SharedFolderManager;
+import org.olat.repository.RepositoryEntry;
+import org.olat.repository.RepositoryEntryImportExport;
+import org.olat.repository.RepositoryManager;
+import org.olat.resource.OLATResource;
+
+/**
+ * 
+ * Initial date: 17 sept. 2019<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class CourseExportMediaResource implements MediaResource {
+	
+	private static Logger log = Tracing.createLoggerFor(CourseExportMediaResource.class);
+	
+	private final OLATResourceable resource;
+	
+	public CourseExportMediaResource(OLATResourceable resource) {
+		this.resource = resource;
+	}
+
+	@Override
+	public boolean acceptRanges() {
+		return false;
+	}
+
+	@Override
+	public long getCacheControlDuration() {
+		return ServletUtil.CACHE_NO_CACHE;
+	}
+
+	@Override
+	public String getContentType() {
+		return "application/zip";
+	}
+
+	@Override
+	public Long getSize() {
+		return null;
+	}
+
+	@Override
+	public InputStream getInputStream() {
+		return null;
+	}
+
+	@Override
+	public Long getLastModified() {
+		return null;
+	}
+
+	@Override
+	public void prepare(HttpServletResponse hres) {
+		try {
+			hres.setCharacterEncoding("UTF-8");
+		} catch (Exception e) {
+			log.error("", e);
+		}
+
+		try(ZipOutputStream zout = new ZipOutputStream(hres.getOutputStream())) {
+			RepositoryEntry entry = RepositoryManager.getInstance().lookupRepositoryEntry(resource, true);
+			String label = StringHelper.transformDisplayNameToFileSystemName(entry.getDisplayname());
+			hres.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + StringHelper.urlEncodeUTF8(label + ".zip"));			
+			hres.setHeader("Content-Description", StringHelper.urlEncodeUTF8(label));
+			exportCourseToZIP(resource, zout);
+		} catch (Exception e) {
+			log.error("", e);
+		}
+	}
+
+	@Override
+	public void release() {
+		//
+	}
+	
+	/**
+	 * Exports an entire course to a zip file.
+	 *
+	 * @param sourceRes
+	 * @param fTargetZIP
+	 * @return true if successfully exported, false otherwise.
+	 */
+	private void exportCourseToZIP(OLATResourceable sourceRes, ZipOutputStream outStream) {
+		PersistingCourseImpl sourceCourse = (PersistingCourseImpl) CourseFactory.loadCourse(sourceRes);
+		log.info("Start course export: {}", sourceRes);
+		synchronized (sourceCourse) { //o_clusterNOK - cannot be solved with doInSync since could take too long (leads to error: "Lock wait timeout exceeded")
+			OLATResource courseResource = sourceCourse.getCourseEnvironment().getCourseGroupManager().getCourseResource();
+			exportToFilesystem(courseResource, sourceCourse, outStream);
+		}
+	}
+	
+	/**
+	 * @see org.olat.course.ICourse#exportToFilesystem(java.io.File)
+	 * <p>
+	 * See OLAT-5368: Course Export can take longer than say 2min.
+	 * <p>
+	 */
+	private void exportToFilesystem(OLATResource originalCourseResource, PersistingCourseImpl sourceCourse, ZipOutputStream outStream) {
+		long s = System.currentTimeMillis();
+		
+		File fExportDir = new File(WebappHelper.getTmpDir(), CodeHelper.getUniqueID());
+		try {
+			fExportDir.mkdirs();
+			log.info("exportToFilesystem: exporting course {} to {}", originalCourseResource, fExportDir);
+			exportToFilesystem(sourceCourse, outStream, fExportDir);
+		} catch(Exception e) {
+			log.error("", e);
+		} finally {
+			FileUtils.deleteDirsAndFiles(fExportDir, true, true);
+		}
+		
+		log.info("exportToFilesystem: exporting course {} to {} done", originalCourseResource, fExportDir);
+		log.info("finished export course '{}' in {}s", sourceCourse.getCourseTitle(), Long.toString((System.currentTimeMillis() - s) / 1000l));
+	}
+
+	private void exportToFilesystem(PersistingCourseImpl sourceCourse, ZipOutputStream zout, File exportDirectory) {
+		LocalFolderImpl courseBaseContainer = sourceCourse.getCourseBaseContainer();
+		File fCourseBase = courseBaseContainer.getBasefile();
+
+		ZipUtil.addFileToZip(CourseConfigManager.COURSECONFIG_XML, new File(fCourseBase, CourseConfigManager.COURSECONFIG_XML), zout);
+		ZipUtil.addFileToZip(PersistingCourseImpl.EDITORTREEMODEL_XML, new File(fCourseBase, PersistingCourseImpl.EDITORTREEMODEL_XML), zout);
+		ZipUtil.addFileToZip(PersistingCourseImpl.RUNSTRUCTURE_XML, new File(fCourseBase, PersistingCourseImpl.RUNSTRUCTURE_XML), zout);
+		// export layout and media folder
+		File layoutDirectory = new File(fCourseBase, "layout");
+		if(layoutDirectory.exists()) {
+			ZipUtil.addPathToZip("layout", layoutDirectory.toPath(), zout);
+		}
+		File mediaDirectory = new File(fCourseBase, "media");
+		if(mediaDirectory.exists()) {
+			ZipUtil.addPathToZip("media", mediaDirectory.toPath(), zout);
+		}
+		
+		try {
+			exportCoursefolder(sourceCourse, zout);
+		} catch (IOException e) {
+			log.error("Cannot zip course folder: {}", sourceCourse, e);
+		}
+
+		//make the folder structure
+		File fExportedDataDir = new File(exportDirectory, ICourse.EXPORTED_DATA_FOLDERNAME);
+		fExportedDataDir.mkdirs();
+		
+		exportBusinessGroupData(sourceCourse, fExportedDataDir, zout);
+
+		// export any node data
+		log.info("exportToFilesystem: exporting course exporting all nodes: {}", sourceCourse);
+		TreeVisitor tv = new TreeVisitor(node -> exportNode(sourceCourse, fExportedDataDir, node, zout),
+				sourceCourse.getEditorTreeModel().getRootNode(), true);
+		tv.visitAll();
+		log.info("exportToFilesystem: exporting all course nodes done: {}", sourceCourse);
+
+		// export shared folder
+		CourseConfig config = sourceCourse.getCourseConfig();
+		if (config.hasCustomSharedFolder()) {
+			exportSharedFolder(config, sourceCourse, fExportedDataDir, zout);
+		}
+		// export glossary
+		if (config.hasGlossary()) {
+			exportGlossary(config, sourceCourse, fExportedDataDir, zout);
+		}
+
+		log.info("exportToFilesystem: exporting course configuration and repo data: {}", sourceCourse);
+
+		exportRepositoryEntryMetadata(sourceCourse, zout);
+		exportReminders(sourceCourse, zout);
+
+		DBFactory.getInstance().commitAndCloseSession();
+	}
+	
+	private void exportReminders(PersistingCourseImpl sourceCourse,  ZipOutputStream zout) {
+		try {
+			zout.putNextEntry(new ZipEntry(ZipUtil.concat(ICourse.EXPORTED_DATA_FOLDERNAME, ReminderService.REMINDERS_XML)));
+
+			RepositoryEntry entry = sourceCourse.getCourseEnvironment().getCourseGroupManager().getCourseEntry();
+			CoreSpringFactory.getImpl(ReminderService.class).exportReminders(entry, zout);
+			
+			zout.closeEntry();
+		} catch(Exception e) {
+			log.error("", e);
+			DBFactory.getInstance().commitAndCloseSession();
+		}
+	}
+	
+	private void exportRepositoryEntryMetadata(PersistingCourseImpl sourceCourse, ZipOutputStream zout) {
+		try {
+			RepositoryEntry entry = RepositoryManager.getInstance().lookupRepositoryEntry(sourceCourse, true);
+			RepositoryEntryImportExport importExport = new RepositoryEntryImportExport(entry, null);
+			importExport.exportDoExportProperties(ICourse.EXPORTED_DATA_FOLDERNAME, zout);
+		} catch (Exception e) {
+			log.error("", e);
+		} finally {
+			DBFactory.getInstance().commitAndCloseSession();
+		}
+	}
+	
+	private void exportGlossary(CourseConfig config, PersistingCourseImpl sourceCourse, File fExportedDataDir, ZipOutputStream zout) {
+		File glossaryExportDataDir = new File(fExportedDataDir, "glossary");
+		try {
+			glossaryExportDataDir.mkdir();
+		
+			log.info("exportToFilesystem: exporting course glossary: {}", sourceCourse);
+			final GlossaryManager glossaryManager = CoreSpringFactory.getImpl(GlossaryManager.class);
+			final CourseConfigManager courseConfigManager = CoreSpringFactory.getImpl(CourseConfigManager.class);
+			if (!glossaryManager.exportGlossary(config.getGlossarySoftKey(), glossaryExportDataDir)) {
+				// export failed, delete reference to glossary in the course config
+				log.info("exportToFilesystem: export of glossary failed.");
+				config.setGlossarySoftKey(null);
+				courseConfigManager.saveConfigTo(sourceCourse, config);
+			}
+			log.info("exportToFilesystem: exporting course glossary done: {}", sourceCourse);
+			
+			ZipUtil.addDirectoryToZip(glossaryExportDataDir.toPath(), ICourse.EXPORTED_DATA_FOLDERNAME, zout);
+		} catch (Exception e) {
+			log.error("", e);
+		} finally {
+			FileUtils.deleteDirsAndFiles(glossaryExportDataDir, true, true);
+			DBFactory.getInstance().commitAndCloseSession();
+		}
+	}
+	
+	private void exportSharedFolder(CourseConfig config, PersistingCourseImpl sourceCourse, File fExportedDataDir, ZipOutputStream zout) {
+		File sharedFolderExportDataDir = new File(fExportedDataDir, "sharedfolder");
+		try {
+			sharedFolderExportDataDir.mkdir();
+		
+			log.info("exportToFilesystem: exporting shared folder course: {}", sourceCourse);
+			if (!SharedFolderManager.getInstance().exportSharedFolder(config.getSharedFolderSoftkey(), sharedFolderExportDataDir)) {
+				// export failed, delete reference to shared folder in the course config
+				log.info("exportToFilesystem: export of shared folder failed.");
+				config.setSharedFolderSoftkey(CourseConfig.VALUE_EMPTY_SHAREDFOLDER_SOFTKEY);
+				CoreSpringFactory.getImpl(CourseConfigManager.class).saveConfigTo(sourceCourse, config);
+			}
+			log.info("exportToFilesystem: exporting shared folder course done: {}", sourceCourse);
+			ZipUtil.addDirectoryToZip(sharedFolderExportDataDir.toPath(), ICourse.EXPORTED_DATA_FOLDERNAME, zout);
+		} catch (Exception e) {
+			log.error("", e);
+		} finally {
+			FileUtils.deleteDirsAndFiles(sharedFolderExportDataDir, true, true);
+			DBFactory.getInstance().commitAndCloseSession();
+		}
+	}
+	
+	private void exportCoursefolder(PersistingCourseImpl sourceCourse, ZipOutputStream zout) throws IOException {
+		File courseFolder = sourceCourse.getIsolatedCourseBaseFolder();
+		File[] hasChildren = courseFolder.listFiles(SystemFileFilter.DIRECTORY_FILES);
+		if(hasChildren != null && hasChildren.length > 0) {
+			zout.putNextEntry(new ZipEntry("oocoursefolder.zip"));
+			
+			// export course folder
+			try(OutputStream shieldedStream = new ShieldOutputStream(zout);
+					ZipOutputStream exportStream = new ZipOutputStream(shieldedStream)) {
+				ZipUtil.addPathToZip(courseFolder.toPath(), exportStream);	
+			} catch(Exception e) {
+				log.error("", e);
+			}
+			
+			zout.closeEntry();
+		}			
+	}
+	
+	private void exportBusinessGroupData(PersistingCourseImpl sourceCourse, File fExportedDataDir, ZipOutputStream zout) {
+		File groupExportDataDir = new File(fExportedDataDir, "groups");
+		try {
+			groupExportDataDir.mkdir();
+			//export business groups
+			CourseEnvironmentMapper envMapper = sourceCourse.getCourseEnvironment().getCourseGroupManager().getBusinessGroupEnvironment();
+			sourceCourse.getCourseEnvironment().getCourseGroupManager().exportCourseBusinessGroups(groupExportDataDir, envMapper);
+			ZipUtil.addDirectoryToZip(groupExportDataDir.toPath(), ICourse.EXPORTED_DATA_FOLDERNAME, zout);
+		} catch (Exception e) {
+			log.error("", e);
+		} finally {
+			FileUtils.deleteDirsAndFiles(groupExportDataDir, true, true);
+			DBFactory.getInstance().commitAndCloseSession();
+		}	
+	}
+	
+	private void exportNode(PersistingCourseImpl sourceCourse, File fExportedDataDir, INode node, ZipOutputStream zout) {
+		CourseEditorTreeNode cNode = (CourseEditorTreeNode) node;
+		CourseNode courseNode = cNode.getCourseNode();
+		if(courseNode instanceof ScormCourseNode
+				|| courseNode instanceof CPCourseNode) {
+			exportCPCourseNode(courseNode, zout);
+		} else if(courseNode instanceof VideoCourseNode) {
+			exportVideoCourseNode((VideoCourseNode)courseNode, zout);
+		} else if(courseNode instanceof BCCourseNode) {
+			exportBCCourseNode(sourceCourse, (BCCourseNode)courseNode, zout);
+		} else {
+			exportCourseNode(sourceCourse, fExportedDataDir, courseNode, zout);
+		}	
+	}
+
+	private void exportCourseNode(PersistingCourseImpl sourceCourse, File fExportedDataDir, CourseNode courseNode, ZipOutputStream zout) {
+		File nodeExportDataDir = new File(fExportedDataDir, "node_" + courseNode.getIdent());
+		try {
+			nodeExportDataDir.mkdir();
+			
+			courseNode.exportNode(nodeExportDataDir, sourceCourse);
+			ZipUtil.addDirectoryToZip(nodeExportDataDir.toPath(), ICourse.EXPORTED_DATA_FOLDERNAME, zout);
+		} catch (Exception e) {
+			log.error("", e);
+		} finally {
+			FileUtils.deleteDirsAndFiles(nodeExportDataDir, true, true);
+			DBFactory.getInstance().commitAndCloseSession();
+		}
+	}
+	
+	private void exportBCCourseNode(PersistingCourseImpl sourceCourse, BCCourseNode courseNode, ZipOutputStream zout) {
+		try(ShieldOutputStream fOut = new ShieldOutputStream(zout)) {
+			VFSContainer nodeContainer = VFSManager.olatRootContainer(BCCourseNode.getFoldernodePathRelToFolderBase(sourceCourse.getCourseEnvironment(), courseNode), null);
+	
+			String nodeDirectory = ZipUtil.concat(ICourse.EXPORTED_DATA_FOLDERNAME, courseNode.getIdent());
+			zout.putNextEntry(new ZipEntry(ZipUtil.concat(nodeDirectory, "oonode.zip")));
+			
+			ZipUtil.zip(nodeContainer, fOut);
+			
+			zout.closeEntry();
+		} catch (IOException e) {
+			log.error("", e);
+		} finally {
+			DBFactory.getInstance().commitAndCloseSession();
+		}
+	}
+	
+	/**
+	 * Export video course node 
+	 * @param courseNode
+	 * @param zout
+	 */
+	private void exportVideoCourseNode(VideoCourseNode courseNode, ZipOutputStream zout) {
+		try {
+			RepositoryEntry videoEntry = VideoEditController.getVideoReference(courseNode.getModuleConfiguration(), false);
+			exportCourseNodeWithRepositoryEntry(videoEntry, courseNode, zout);
+		} catch(Exception e) {
+			log.error("", e);
+		} finally {
+			DBFactory.getInstance().commitAndCloseSession();
+		}
+	}
+	
+	/**
+	 * Export course node with IMS CP or SCORM packages
+	 * @param courseNode
+	 * @param zout
+	 */
+	private void exportCPCourseNode(CourseNode courseNode, ZipOutputStream zout) {
+		try {
+			RepositoryEntry scormEntry = CPEditController.getCPReference(courseNode.getModuleConfiguration(), false);
+			exportCourseNodeWithRepositoryEntry(scormEntry, courseNode, zout);
+		} catch(Exception e) {
+			log.error("", e);
+		} finally {
+			DBFactory.getInstance().commitAndCloseSession();
+		}
+	}
+	
+	private void exportCourseNodeWithRepositoryEntry(RepositoryEntry entry, CourseNode courseNode, ZipOutputStream zout) {
+		if(entry != null) {
+			RepositoryEntryImportExport reie = new RepositoryEntryImportExport(entry, null);
+			reie.exportDoExport(ZipUtil.concat(ICourse.EXPORTED_DATA_FOLDERNAME, courseNode.getIdent()), zout);
+		}
+	}
+}
diff --git a/src/main/java/org/olat/course/groupsandrights/CourseGroupManager.java b/src/main/java/org/olat/course/groupsandrights/CourseGroupManager.java
index bf41516382d..2ee6bf84331 100644
--- a/src/main/java/org/olat/course/groupsandrights/CourseGroupManager.java
+++ b/src/main/java/org/olat/course/groupsandrights/CourseGroupManager.java
@@ -263,7 +263,7 @@ public interface CourseGroupManager {
 	 * 
 	 * @param fExportDirectory
 	 */
-	public void exportCourseBusinessGroups(File fExportDirectory, CourseEnvironmentMapper env, boolean runtimeDatas);
+	public void exportCourseBusinessGroups(File fExportDirectory, CourseEnvironmentMapper env);
 	
 	public CourseEnvironmentMapper getBusinessGroupEnvironment();
 
diff --git a/src/main/java/org/olat/course/groupsandrights/PersistingCourseGroupManager.java b/src/main/java/org/olat/course/groupsandrights/PersistingCourseGroupManager.java
index 61853b79d99..d0754bb7b27 100644
--- a/src/main/java/org/olat/course/groupsandrights/PersistingCourseGroupManager.java
+++ b/src/main/java/org/olat/course/groupsandrights/PersistingCourseGroupManager.java
@@ -367,7 +367,7 @@ public class PersistingCourseGroupManager implements CourseGroupManager {
 	}
 
 	@Override
-	public void exportCourseBusinessGroups(File fExportDirectory, CourseEnvironmentMapper courseEnv, boolean runtimeDatas) {
+	public void exportCourseBusinessGroups(File fExportDirectory, CourseEnvironmentMapper courseEnv) {
 		File fExportFile = new File(fExportDirectory, LEARNINGGROUPEXPORT_XML);
 		List<BGArea> areas = getAllAreas();
 		List<BusinessGroup> groups = getAllBusinessGroups();
@@ -375,7 +375,7 @@ public class PersistingCourseGroupManager implements CourseGroupManager {
 		BusinessGroupEnvironment bgEnv = new BusinessGroupEnvironment();
 		bgEnv.getGroups().addAll(courseEnv.getGroups());
 		bgEnv.getAreas().addAll(courseEnv.getAreas());
-		businessGroupService.exportGroups(groups, areas, fExportFile, runtimeDatas);
+		businessGroupService.exportGroups(groups, areas, fExportFile, false);
 	}
 	
 	/**
diff --git a/src/main/java/org/olat/course/run/preview/PreviewCourseGroupManager.java b/src/main/java/org/olat/course/run/preview/PreviewCourseGroupManager.java
index 8ed18c299bb..9d87eb121e2 100644
--- a/src/main/java/org/olat/course/run/preview/PreviewCourseGroupManager.java
+++ b/src/main/java/org/olat/course/run/preview/PreviewCourseGroupManager.java
@@ -373,7 +373,7 @@ final class PreviewCourseGroupManager implements CourseGroupManager {
 	}
 
 	@Override
-	public void exportCourseBusinessGroups(File fExportDirectory, CourseEnvironmentMapper env, boolean runtimeDatas) {
+	public void exportCourseBusinessGroups(File fExportDirectory, CourseEnvironmentMapper env) {
 		throw new AssertException("unsupported");
 	}
 
diff --git a/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateMediaResource.java b/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateMediaResource.java
index 6b151b81105..49d4f3b2797 100644
--- a/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateMediaResource.java
+++ b/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateMediaResource.java
@@ -131,7 +131,7 @@ public class BinderTemplateMediaResource implements MediaResource {
 			OLATResource resource = templateEntry.getOlatResource();
 			File baseContainer= FileResourceManager.getInstance().getFileResource(resource);
 			RepositoryEntryImportExport importExport = new RepositoryEntryImportExport(templateEntry, baseContainer);
-			importExport.exportDoExportProperties(zout);
+			importExport.exportDoExportProperties("", zout);
 		} catch (Exception e) {
 			log.error("", e);
 		}
diff --git a/src/main/java/org/olat/modules/reminder/ReminderService.java b/src/main/java/org/olat/modules/reminder/ReminderService.java
index 67ef306189a..7e46745a956 100644
--- a/src/main/java/org/olat/modules/reminder/ReminderService.java
+++ b/src/main/java/org/olat/modules/reminder/ReminderService.java
@@ -20,6 +20,7 @@
 package org.olat.modules.reminder;
 
 import java.io.File;
+import java.io.OutputStream;
 import java.util.List;
 
 import org.olat.core.id.Identity;
@@ -85,7 +86,7 @@ public interface ReminderService {
 	
 	public ReminderRules toRules(String rulesXml);
 	
-	public void exportReminders(RepositoryEntry entry, File fExportedDataDir);
+	public void exportReminders(RepositoryEntryRef entry, OutputStream fExportedDataDir);
 	
 	/**
 	 * The reminders are not persisted and not converted to any new course, group...
diff --git a/src/main/java/org/olat/modules/reminder/manager/ReminderServiceImpl.java b/src/main/java/org/olat/modules/reminder/manager/ReminderServiceImpl.java
index e406a07fd0a..fd97b219c23 100644
--- a/src/main/java/org/olat/modules/reminder/manager/ReminderServiceImpl.java
+++ b/src/main/java/org/olat/modules/reminder/manager/ReminderServiceImpl.java
@@ -21,7 +21,6 @@ package org.olat.modules.reminder.manager;
 
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.text.ParseException;
@@ -31,13 +30,13 @@ import java.util.List;
 import java.util.Locale;
 import java.util.UUID;
 
+import org.apache.logging.log4j.Logger;
 import org.apache.velocity.VelocityContext;
 import org.olat.core.gui.translator.Translator;
 import org.olat.core.helpers.Settings;
 import org.olat.core.id.Identity;
 import org.olat.core.id.User;
 import org.olat.core.id.UserConstants;
-import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.StringHelper;
@@ -173,19 +172,17 @@ public class ReminderServiceImpl implements ReminderService {
 	}
 	
 	@Override
-	public void exportReminders(RepositoryEntry entry, File fExportedDataDir) {
+	public void exportReminders(RepositoryEntryRef entry, OutputStream fOut) {
 		List<Reminder> reminders = reminderDao.getReminders(entry);
-		if(reminders.size() > 0) {
-			try (OutputStream fOut = new FileOutputStream(new File(fExportedDataDir, REMINDERS_XML))) {
-				ImportExportReminders exportReminders = new ImportExportReminders();
-				for(Reminder reminder:reminders) {
-					ImportExportReminder exportReminder = new ImportExportReminder(reminder);
-					exportReminders.getReminders().add(exportReminder);
-				}
-				ReminderRulesXStream.toXML(exportReminders, fOut);
-			} catch(Exception e) {
-				log.error("", e);
+		try {
+			ImportExportReminders exportReminders = new ImportExportReminders();
+			for(Reminder reminder:reminders) {
+				ImportExportReminder exportReminder = new ImportExportReminder(reminder);
+				exportReminders.getReminders().add(exportReminder);
 			}
+			ReminderRulesXStream.toXML(exportReminders, fOut);
+		} catch(Exception e) {
+			log.error("", e);
 		}
 	}
 
diff --git a/src/main/java/org/olat/repository/RepositoryEntryImportExport.java b/src/main/java/org/olat/repository/RepositoryEntryImportExport.java
index eced9bc3c81..dacf8b033a8 100644
--- a/src/main/java/org/olat/repository/RepositoryEntryImportExport.java
+++ b/src/main/java/org/olat/repository/RepositoryEntryImportExport.java
@@ -41,6 +41,7 @@ import java.util.zip.ZipOutputStream;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.commons.io.IOUtils;
+import org.apache.logging.log4j.Logger;
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.services.license.LicenseService;
 import org.olat.core.commons.services.license.LicenseType;
@@ -48,10 +49,10 @@ import org.olat.core.commons.services.license.ResourceLicense;
 import org.olat.core.commons.services.license.ui.LicenseUIFactory;
 import org.olat.core.gui.media.MediaResource;
 import org.olat.core.logging.OLATRuntimeException;
-import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.FileUtils;
 import org.olat.core.util.StringHelper;
+import org.olat.core.util.ZipUtil;
 import org.olat.core.util.io.HttpServletResponseOutputStream;
 import org.olat.core.util.io.ShieldOutputStream;
 import org.olat.core.util.vfs.LocalFileImpl;
@@ -141,6 +142,39 @@ public class RepositoryEntryImportExport {
 		exportDoExportProperties();
 		return exportDoExportContent();
 	}
+	
+	public boolean exportDoExport(String zipPath, ZipOutputStream zout) {
+		exportDoExportProperties(zipPath, zout);
+		return exportDoExportContent(zipPath, zout);
+	}
+	
+	public void exportDoExportProperties(String zipPath, ZipOutputStream zout) {
+		// save repository entry properties
+		RepositoryEntryImport imp = new RepositoryEntryImport(re);
+		RepositoryManager rm = RepositoryManager.getInstance();
+		VFSLeaf image = rm.getImage(re);
+		if(image instanceof LocalFileImpl) {
+			imp.setImageName(image.getName());
+			ZipUtil.addFileToZip(ZipUtil.concat(zipPath, image.getName()) , ((LocalFileImpl)image).getBasefile(), zout);
+		}
+
+		RepositoryService repositoryService = CoreSpringFactory.getImpl(RepositoryService.class);
+		VFSLeaf movie = repositoryService.getIntroductionMovie(re);
+		if(movie instanceof LocalFileImpl) {
+			imp.setMovieName(movie.getName());
+			ZipUtil.addFileToZip(ZipUtil.concat(zipPath, movie.getName()), ((LocalFileImpl)movie).getBasefile(), zout);
+		}
+		
+		try(ShieldOutputStream fOut = new ShieldOutputStream(zout)) {
+			addLicenseInformations(imp, re);
+			zout.putNextEntry(new ZipEntry(ZipUtil.concat(zipPath, PROPERTIES_FILE)));
+			xstream.toXML(imp, fOut);
+			zout.closeEntry();
+		} catch (IOException ioe) {
+			throw new OLATRuntimeException("Error writing repo properties.", ioe);
+		}
+	}
+	
 	/**
 	 * Export metadata of a repository entry to a file.
 	 * Only one repository entry's metadata may be exported into a directory. The
@@ -184,45 +218,6 @@ public class RepositoryEntryImportExport {
 		}
 	}
 
-	public void exportDoExportProperties(ZipOutputStream zout) throws IOException {
-		RepositoryEntryImport imp = new RepositoryEntryImport(re);
-		RepositoryManager rm = RepositoryManager.getInstance();
-		VFSLeaf image = rm.getImage(re);
-		if(image != null) {
-			imp.setImageName(image.getName());
-			zout.putNextEntry(new ZipEntry(image.getName()));
-			try(InputStream inImage=image.getInputStream();
-					OutputStream out = new ShieldOutputStream(zout)) {
-				FileUtils.copy(inImage, out);
-			} catch(Exception e) {
-				log.error("", e);
-			}
-			zout.closeEntry();
-		}
-
-		RepositoryService repositoryService = CoreSpringFactory.getImpl(RepositoryService.class);
-		VFSLeaf movie = repositoryService.getIntroductionMovie(re);
-		if(movie != null) {
-			imp.setMovieName(movie.getName());
-			zout.putNextEntry(new ZipEntry(movie.getName()));
-			try(InputStream inMovie=movie.getInputStream();
-					OutputStream out=new ShieldOutputStream(zout)) {
-				FileUtils.copy(inMovie, out);
-			} catch(Exception e) {
-				log.error("", e);
-			}
-			zout.closeEntry();
-		}
-		
-		zout.putNextEntry(new ZipEntry(PROPERTIES_FILE));
-		try(OutputStream out=new ShieldOutputStream(zout)) {
-			xstream.toXML(imp, out);
-		} catch(IOException e) {
-			log.error("", e);
-		}
-		zout.closeEntry();
-	}
-
 	/**
 	 * Export a repository entry referenced by a course node to the given export directory.
 	 * User importReferencedRepositoryEntry to import again.
@@ -250,6 +245,29 @@ public class RepositoryEntryImportExport {
 		return true;
 	}
 	
+	public boolean exportDoExportContent(String zipPath, ZipOutputStream zout) {
+		// export resource
+		RepositoryHandler rh = RepositoryHandlerFactory.getInstance().getRepositoryHandler(re);
+		MediaResource mr = rh.getAsMediaResource(re.getOlatResource());
+		try(OutputStream fOut = new ShieldOutputStream(zout);
+				InputStream in = mr.getInputStream()) {
+			zout.putNextEntry(new ZipEntry(ZipUtil.concat(zipPath, CONTENT_FILE)));
+			if(in == null) {
+				HttpServletResponse hres = new HttpServletResponseOutputStream(fOut);
+				mr.prepare(hres);	
+			} else {
+				IOUtils.copy(in, fOut);
+			}
+			fOut.flush();
+			zout.closeEntry();
+		} catch (IOException fnfe) {
+			return false;
+		} finally {
+			mr.release();
+		}
+		return true;
+	}
+	
 	public RepositoryEntry importContent(RepositoryEntry newEntry, VFSContainer mediaContainer) {
 		if(!anyExportedPropertiesAvailable()) return newEntry;
 
diff --git a/src/main/java/org/olat/repository/handlers/CourseHandler.java b/src/main/java/org/olat/repository/handlers/CourseHandler.java
index 4deba24665c..91c3fc64321 100644
--- a/src/main/java/org/olat/repository/handlers/CourseHandler.java
+++ b/src/main/java/org/olat/repository/handlers/CourseHandler.java
@@ -50,7 +50,6 @@ import org.olat.core.gui.control.generic.layout.MainLayoutController;
 import org.olat.core.gui.control.generic.wizard.Step;
 import org.olat.core.gui.control.generic.wizard.StepRunnerCallback;
 import org.olat.core.gui.control.generic.wizard.StepsMainRunController;
-import org.olat.core.gui.media.CleanupAfterDeliveryFileMediaResource;
 import org.olat.core.gui.media.MediaResource;
 import org.olat.core.gui.translator.Translator;
 import org.olat.core.helpers.Settings;
@@ -62,7 +61,6 @@ import org.olat.core.logging.Tracing;
 import org.olat.core.util.FileUtils;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.PathUtils;
-import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
 import org.olat.core.util.WebappHelper;
 import org.olat.core.util.ZipUtil;
@@ -85,6 +83,7 @@ import org.olat.course.Structure;
 import org.olat.course.config.CourseConfig;
 import org.olat.course.editor.CourseAccessAndProperties;
 import org.olat.course.export.CourseEnvironmentMapper;
+import org.olat.course.export.CourseExportMediaResource;
 import org.olat.course.groupsandrights.CourseGroupManager;
 import org.olat.course.groupsandrights.PersistingCourseGroupManager;
 import org.olat.course.run.CourseRuntimeController;
@@ -461,7 +460,7 @@ public class CourseHandler implements RepositoryHandler {
 			
 		File fExportDir = new File(WebappHelper.getTmpDir(), UUID.randomUUID().toString());
 		fExportDir.mkdirs();
-		sourceCgm.exportCourseBusinessGroups(fExportDir, env, false);
+		sourceCgm.exportCourseBusinessGroups(fExportDir, env);
 
 		ICourse course = CourseFactory.loadCourse(target);
 		CourseGroupManager cgm = course.getCourseEnvironment().getCourseGroupManager();
@@ -546,11 +545,7 @@ public class CourseHandler implements RepositoryHandler {
 
 	@Override
 	public MediaResource getAsMediaResource(OLATResourceable res) {
-		RepositoryEntry re = RepositoryManager.getInstance().lookupRepositoryEntry(res, true);
-		String exportFileName = StringHelper.transformDisplayNameToFileSystemName(re.getDisplayname()) + ".zip";
-		File fExportZIP = new File(WebappHelper.getTmpDir(), exportFileName);
-		CourseFactory.exportCourseToZIP(res, fExportZIP, false);
-		return new CleanupAfterDeliveryFileMediaResource(fExportZIP);
+		return new CourseExportMediaResource(res);
 	}
 	
 	@Override
-- 
GitLab