Unverified Commit 127c9611 authored by Bart Mathijssen's avatar Bart Mathijssen Committed by GitHub

Allow for a subset of projects to be exported (#2051)

Co-authored-by: default avatarAaron Traylor <attraylor@gmail.com>
parent 83eacf43
......@@ -476,6 +476,10 @@ public interface OdeMessages extends Messages, AutogeneratedOdeMessages {
@Description("Name of Export Project menuitem")
String exportProjectMenuItem();
@DefaultMessage("Export {0} selected projects")
@Description("Name of Export Selected Projects menuitem")
String exportSelectedProjectsMenuItem(int numSelectedProjects);
@DefaultMessage("Export all projects")
@Description("Name of Export all Project menuitem")
String exportAllProjectsMenuItem();
......
......@@ -186,8 +186,11 @@ public class TopToolbar extends Composite {
boolean allowDelete = !isReadOnly && numSelectedProjects > 0;
boolean allowExport = numSelectedProjects > 0;
boolean allowExportAll = numProjects > 0;
String exportProjectLabel = numSelectedProjects > 1 ?
MESSAGES.exportSelectedProjectsMenuItem(numSelectedProjects) : MESSAGES.exportProjectMenuItem();
fileDropDown.setItemHtmlById(WIDGET_NAME_EXPORTPROJECT, exportProjectLabel);
fileDropDown.setItemEnabled(MESSAGES.deleteProjectMenuItem(), allowDelete);
fileDropDown.setItemEnabled(MESSAGES.exportProjectMenuItem(), allowExport);
fileDropDown.setItemEnabledById(WIDGET_NAME_EXPORTPROJECT, allowExport);
fileDropDown.setItemEnabled(MESSAGES.exportAllProjectsMenuItem(), allowExportAll);
}
......@@ -572,6 +575,8 @@ public class TopToolbar extends Composite {
//If we are in the projects view
if (selectedProjects.size() == 1) {
exportProject(selectedProjects.get(0));
} else if (selectedProjects.size() > 1) {
exportSelectedProjects(selectedProjects);
} else {
// The user needs to select only one project.
ErrorReporter.reportInfo(MESSAGES.wrongNumberProjectsSelected());
......@@ -589,6 +594,20 @@ public class TopToolbar extends Composite {
Downloader.getInstance().download(ServerLayout.DOWNLOAD_SERVLET_BASE +
ServerLayout.DOWNLOAD_PROJECT_SOURCE + "/" + project.getProjectId());
}
private void exportSelectedProjects(List<Project> projects) {
Tracking.trackEvent(Tracking.PROJECT_EVENT,
Tracking.PROJECT_ACTION_DOWNLOAD_SELECTED_PROJECTS_SOURCE_YA);
String selectedProjPath = ServerLayout.DOWNLOAD_SERVLET_BASE +
ServerLayout.DOWNLOAD_SELECTED_PROJECTS_SOURCE + "/";
for (Project project : projects) {
selectedProjPath += project.getProjectId() + "-";
}
Downloader.getInstance().download(selectedProjPath);
}
}
private static class ExportAllProjectsAction implements Command {
......@@ -1069,7 +1088,7 @@ public class TopToolbar extends Composite {
Ode.getInstance().getProjectManager().getProjects() == null);
fileDropDown.setItemEnabled(MESSAGES.exportAllProjectsMenuItem(),
Ode.getInstance().getProjectManager().getProjects().size() > 0);
fileDropDown.setItemEnabled(MESSAGES.exportProjectMenuItem(), false);
fileDropDown.setItemEnabledById(WIDGET_NAME_EXPORTPROJECT, false);
fileDropDown.setItemEnabled(MESSAGES.saveMenuItem(), false);
fileDropDown.setItemEnabled(MESSAGES.saveAsMenuItem(), false);
fileDropDown.setItemEnabled(MESSAGES.checkpointMenuItem(), false);
......@@ -1079,7 +1098,7 @@ public class TopToolbar extends Composite {
fileDropDown.setItemEnabled(MESSAGES.deleteProjectButton(), true);
fileDropDown.setItemEnabled(MESSAGES.exportAllProjectsMenuItem(),
Ode.getInstance().getProjectManager().getProjects().size() > 0);
fileDropDown.setItemEnabled(MESSAGES.exportProjectMenuItem(), true);
fileDropDown.setItemEnabledById(WIDGET_NAME_EXPORTPROJECT, true);
fileDropDown.setItemEnabled(MESSAGES.saveMenuItem(), true);
fileDropDown.setItemEnabled(MESSAGES.saveAsMenuItem(), true);
fileDropDown.setItemEnabled(MESSAGES.checkpointMenuItem(), true);
......
......@@ -43,6 +43,8 @@ public class Tracking {
"DownloadProjectSource-YA";
public static final String PROJECT_ACTION_DOWNLOAD_FILE_YA = PROJECT_ACTION_PREFIX +
"DownloadFile-YA";
public static final String PROJECT_ACTION_DOWNLOAD_SELECTED_PROJECTS_SOURCE_YA =
PROJECT_ACTION_PREFIX + "DownloadSelectedProjectsSource-YA";
public static final String PROJECT_ACTION_DOWNLOAD_ALL_PROJECTS_SOURCE_YA =
PROJECT_ACTION_PREFIX + "DownloadAllProjectsSource-YA";
public static final String PROJECT_ACTION_SAVE_YA = PROJECT_ACTION_PREFIX +
......
......@@ -20,6 +20,8 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.logging.Logger;
......@@ -42,7 +44,7 @@ public class DownloadServlet extends OdeServlet {
// Constants for accessing split URI
/*
* Download kind can be: "project-output", "project-source",
* "all-projects-source", "file", or "userfile".
* "selected-projects-source", "all-projects-source", "file", or "userfile".
* Constants for these are defined in ServerLayout.
*/
private static final int DOWNLOAD_KIND_INDEX = 3;
......@@ -164,7 +166,7 @@ public class DownloadServlet extends OdeServlet {
projectName = storageIo.getProjectName(projectUserId, projectId);
} catch (NumberFormatException e) {
// assume we got a name instead
for (Long pid: storageIo.getProjects(projectUserId)) {
for (Long pid : storageIo.getProjects(projectUserId)) {
if (storageIo.getProjectName(projectUserId, pid).equals(projectIdOrName)) {
projectId = pid;
}
......@@ -186,7 +188,15 @@ public class DownloadServlet extends OdeServlet {
ProjectSourceZip zipFile = fileExporter.exportProjectSourceZip(projectUserId,
projectId, /* include history*/ true, /* include keystore */ true, zipName, true, true, false, false);
downloadableFile = zipFile.getRawFile();
} else if (downloadKind.equals(ServerLayout.DOWNLOAD_SELECTED_PROJECTS_SOURCE)) {
String[] projectIdStrings = uriComponents[PROJECT_ID_INDEX].split("-");
List<Long> projectIds = new ArrayList<Long>();
for (String projectId : projectIdStrings) {
projectIds.add(Long.valueOf(projectId));
}
ProjectSourceZip zipFile = fileExporter.exportSelectedProjectsSourceZip(
userId, "selected-projects.zip", projectIds);
downloadableFile = zipFile.getRawFile();
} else if (downloadKind.equals(ServerLayout.DOWNLOAD_ALL_PROJECTS_SOURCE)) {
// Download all project source files as a zip of zips.
ProjectSourceZip zipFile = fileExporter.exportAllProjectsSourceZip(
......
......@@ -10,6 +10,7 @@ import com.google.appinventor.shared.rpc.project.ProjectSourceZip;
import com.google.appinventor.shared.rpc.project.RawFile;
import java.io.IOException;
import java.util.List;
import javax.annotation.Nullable;
......@@ -57,6 +58,19 @@ public interface FileExporter {
boolean includeScreenShots,
boolean fatalError, boolean forGallery) throws IOException;
/**
* Exports projects selected by the user as a zip of zips.
*
* @param userId the userId
* @param zipName the desired name for the zip
* @param projectIds the list of project ids corresponding to selected projects
* @return the name, contents, and number of files in the zip
* @throws IllegalArgumentException if download request cannot be fulfilled
* (no projects)
* @throws IOException if files cannot be written
*/
ProjectSourceZip exportSelectedProjectsSourceZip(String userId, String zipName, List<Long> projectIds) throws IOException;
/**
* Exports all of the user's projects' source files as a zip of zips.
*
......
......@@ -76,6 +76,73 @@ public final class FileExporterImpl implements FileExporter {
}
}
@Override
public ProjectSourceZip exportSelectedProjectsSourceZip(String userId,
String zipName, List<Long> projectIds) throws IOException {
// Create a zip file for each project's sources.
if (projectIds.size() == 0) {
throw new IllegalArgumentException("No projects to download");
}
ByteArrayOutputStream zipFile = new ByteArrayOutputStream();
ZipOutputStream out = new ZipOutputStream(zipFile);
int count = 0;
String metadata = "";
for (Long projectId : projectIds) {
try {
ProjectSourceZip projectSourceZip =
exportProjectSourceZip(userId, projectId, false, false, null, false, false, false, false);
byte[] data = projectSourceZip.getContent();
String name = projectSourceZip.getFileName();
// If necessary, renae duplicate projects
while (true) {
try {
out.putNextEntry(new ZipEntry(name));
break;
} catch (IOException e) {
name = "duplicate-" + name;
}
}
metadata += projectSourceZip.getMetadata() + "\n";
out.write(data, 0, data.length);
out.closeEntry();
count++;
} catch (IllegalArgumentException e) {
System.err.println("No files found for userid: " + userId +
" for projectid: " + projectId);
} catch (IOException e) {
System.err.println("IOException while reading files found for userid: " +
userId + " for projectid: " + projectId);
continue;
}
}
if (count == 0) {
throw new IllegalArgumentException("No files to download");
}
List<String> userFiles = storageIo.getUserFiles(userId);
if (userFiles.contains(StorageUtil.ANDROID_KEYSTORE_FILENAME)) {
byte[] androidKeystoreBytes =
storageIo.downloadRawUserFile(userId, StorageUtil.ANDROID_KEYSTORE_FILENAME);
if (androidKeystoreBytes.length > 0) {
out.putNextEntry(new ZipEntry(StorageUtil.ANDROID_KEYSTORE_FILENAME));
out.write(androidKeystoreBytes, 0, androidKeystoreBytes.length);
out.closeEntry();
count++;
}
}
out.close();
// Package the big zip file up as a ProjectSourceZip and return it.
byte[] content = zipFile.toByteArray();
ProjectSourceZip projectSourceZip = new ProjectSourceZip(zipName, content, count);
projectSourceZip.setMetadata(metadata);
return projectSourceZip;
}
@Override
public ProjectSourceZip exportAllProjectsSourceZip(String userId,
String zipName) throws IOException {
......
......@@ -101,6 +101,12 @@ public class ServerLayout {
*/
public static final String DOWNLOAD_PROJECT_SOURCE = "project-source";
/**
* Relative path within {@link com.google.appinventor.server.DownloadServlet}
* for downloading selected of a user's projects' sources.
*/
public static final String DOWNLOAD_SELECTED_PROJECTS_SOURCE = "selected-projects-source";
/**
* Relative path within {@link com.google.appinventor.server.DownloadServlet}
* for downloading all of a user's projects' sources.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment