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 { ...@@ -476,6 +476,10 @@ public interface OdeMessages extends Messages, AutogeneratedOdeMessages {
@Description("Name of Export Project menuitem") @Description("Name of Export Project menuitem")
String exportProjectMenuItem(); String exportProjectMenuItem();
@DefaultMessage("Export {0} selected projects")
@Description("Name of Export Selected Projects menuitem")
String exportSelectedProjectsMenuItem(int numSelectedProjects);
@DefaultMessage("Export all projects") @DefaultMessage("Export all projects")
@Description("Name of Export all Project menuitem") @Description("Name of Export all Project menuitem")
String exportAllProjectsMenuItem(); String exportAllProjectsMenuItem();
......
...@@ -186,8 +186,11 @@ public class TopToolbar extends Composite { ...@@ -186,8 +186,11 @@ public class TopToolbar extends Composite {
boolean allowDelete = !isReadOnly && numSelectedProjects > 0; boolean allowDelete = !isReadOnly && numSelectedProjects > 0;
boolean allowExport = numSelectedProjects > 0; boolean allowExport = numSelectedProjects > 0;
boolean allowExportAll = numProjects > 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.deleteProjectMenuItem(), allowDelete);
fileDropDown.setItemEnabled(MESSAGES.exportProjectMenuItem(), allowExport); fileDropDown.setItemEnabledById(WIDGET_NAME_EXPORTPROJECT, allowExport);
fileDropDown.setItemEnabled(MESSAGES.exportAllProjectsMenuItem(), allowExportAll); fileDropDown.setItemEnabled(MESSAGES.exportAllProjectsMenuItem(), allowExportAll);
} }
...@@ -572,6 +575,8 @@ public class TopToolbar extends Composite { ...@@ -572,6 +575,8 @@ public class TopToolbar extends Composite {
//If we are in the projects view //If we are in the projects view
if (selectedProjects.size() == 1) { if (selectedProjects.size() == 1) {
exportProject(selectedProjects.get(0)); exportProject(selectedProjects.get(0));
} else if (selectedProjects.size() > 1) {
exportSelectedProjects(selectedProjects);
} else { } else {
// The user needs to select only one project. // The user needs to select only one project.
ErrorReporter.reportInfo(MESSAGES.wrongNumberProjectsSelected()); ErrorReporter.reportInfo(MESSAGES.wrongNumberProjectsSelected());
...@@ -589,6 +594,20 @@ public class TopToolbar extends Composite { ...@@ -589,6 +594,20 @@ public class TopToolbar extends Composite {
Downloader.getInstance().download(ServerLayout.DOWNLOAD_SERVLET_BASE + Downloader.getInstance().download(ServerLayout.DOWNLOAD_SERVLET_BASE +
ServerLayout.DOWNLOAD_PROJECT_SOURCE + "/" + project.getProjectId()); 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 { private static class ExportAllProjectsAction implements Command {
...@@ -1069,7 +1088,7 @@ public class TopToolbar extends Composite { ...@@ -1069,7 +1088,7 @@ public class TopToolbar extends Composite {
Ode.getInstance().getProjectManager().getProjects() == null); Ode.getInstance().getProjectManager().getProjects() == null);
fileDropDown.setItemEnabled(MESSAGES.exportAllProjectsMenuItem(), fileDropDown.setItemEnabled(MESSAGES.exportAllProjectsMenuItem(),
Ode.getInstance().getProjectManager().getProjects().size() > 0); 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.saveMenuItem(), false);
fileDropDown.setItemEnabled(MESSAGES.saveAsMenuItem(), false); fileDropDown.setItemEnabled(MESSAGES.saveAsMenuItem(), false);
fileDropDown.setItemEnabled(MESSAGES.checkpointMenuItem(), false); fileDropDown.setItemEnabled(MESSAGES.checkpointMenuItem(), false);
...@@ -1079,7 +1098,7 @@ public class TopToolbar extends Composite { ...@@ -1079,7 +1098,7 @@ public class TopToolbar extends Composite {
fileDropDown.setItemEnabled(MESSAGES.deleteProjectButton(), true); fileDropDown.setItemEnabled(MESSAGES.deleteProjectButton(), true);
fileDropDown.setItemEnabled(MESSAGES.exportAllProjectsMenuItem(), fileDropDown.setItemEnabled(MESSAGES.exportAllProjectsMenuItem(),
Ode.getInstance().getProjectManager().getProjects().size() > 0); 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.saveMenuItem(), true);
fileDropDown.setItemEnabled(MESSAGES.saveAsMenuItem(), true); fileDropDown.setItemEnabled(MESSAGES.saveAsMenuItem(), true);
fileDropDown.setItemEnabled(MESSAGES.checkpointMenuItem(), true); fileDropDown.setItemEnabled(MESSAGES.checkpointMenuItem(), true);
......
...@@ -43,6 +43,8 @@ public class Tracking { ...@@ -43,6 +43,8 @@ public class Tracking {
"DownloadProjectSource-YA"; "DownloadProjectSource-YA";
public static final String PROJECT_ACTION_DOWNLOAD_FILE_YA = PROJECT_ACTION_PREFIX + public static final String PROJECT_ACTION_DOWNLOAD_FILE_YA = PROJECT_ACTION_PREFIX +
"DownloadFile-YA"; "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 = public static final String PROJECT_ACTION_DOWNLOAD_ALL_PROJECTS_SOURCE_YA =
PROJECT_ACTION_PREFIX + "DownloadAllProjectsSource-YA"; PROJECT_ACTION_PREFIX + "DownloadAllProjectsSource-YA";
public static final String PROJECT_ACTION_SAVE_YA = PROJECT_ACTION_PREFIX + public static final String PROJECT_ACTION_SAVE_YA = PROJECT_ACTION_PREFIX +
......
...@@ -20,6 +20,8 @@ import javax.servlet.ServletOutputStream; ...@@ -20,6 +20,8 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.logging.Logger; import java.util.logging.Logger;
...@@ -42,7 +44,7 @@ public class DownloadServlet extends OdeServlet { ...@@ -42,7 +44,7 @@ public class DownloadServlet extends OdeServlet {
// Constants for accessing split URI // Constants for accessing split URI
/* /*
* Download kind can be: "project-output", "project-source", * 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. * Constants for these are defined in ServerLayout.
*/ */
private static final int DOWNLOAD_KIND_INDEX = 3; private static final int DOWNLOAD_KIND_INDEX = 3;
...@@ -164,7 +166,7 @@ public class DownloadServlet extends OdeServlet { ...@@ -164,7 +166,7 @@ public class DownloadServlet extends OdeServlet {
projectName = storageIo.getProjectName(projectUserId, projectId); projectName = storageIo.getProjectName(projectUserId, projectId);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// assume we got a name instead // 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)) { if (storageIo.getProjectName(projectUserId, pid).equals(projectIdOrName)) {
projectId = pid; projectId = pid;
} }
...@@ -186,7 +188,15 @@ public class DownloadServlet extends OdeServlet { ...@@ -186,7 +188,15 @@ public class DownloadServlet extends OdeServlet {
ProjectSourceZip zipFile = fileExporter.exportProjectSourceZip(projectUserId, ProjectSourceZip zipFile = fileExporter.exportProjectSourceZip(projectUserId,
projectId, /* include history*/ true, /* include keystore */ true, zipName, true, true, false, false); projectId, /* include history*/ true, /* include keystore */ true, zipName, true, true, false, false);
downloadableFile = zipFile.getRawFile(); 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)) { } else if (downloadKind.equals(ServerLayout.DOWNLOAD_ALL_PROJECTS_SOURCE)) {
// Download all project source files as a zip of zips. // Download all project source files as a zip of zips.
ProjectSourceZip zipFile = fileExporter.exportAllProjectsSourceZip( ProjectSourceZip zipFile = fileExporter.exportAllProjectsSourceZip(
......
...@@ -10,6 +10,7 @@ import com.google.appinventor.shared.rpc.project.ProjectSourceZip; ...@@ -10,6 +10,7 @@ import com.google.appinventor.shared.rpc.project.ProjectSourceZip;
import com.google.appinventor.shared.rpc.project.RawFile; import com.google.appinventor.shared.rpc.project.RawFile;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
...@@ -57,6 +58,19 @@ public interface FileExporter { ...@@ -57,6 +58,19 @@ public interface FileExporter {
boolean includeScreenShots, boolean includeScreenShots,
boolean fatalError, boolean forGallery) throws IOException; 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. * Exports all of the user's projects' source files as a zip of zips.
* *
......
...@@ -76,6 +76,73 @@ public final class FileExporterImpl implements FileExporter { ...@@ -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 @Override
public ProjectSourceZip exportAllProjectsSourceZip(String userId, public ProjectSourceZip exportAllProjectsSourceZip(String userId,
String zipName) throws IOException { String zipName) throws IOException {
......
...@@ -101,6 +101,12 @@ public class ServerLayout { ...@@ -101,6 +101,12 @@ public class ServerLayout {
*/ */
public static final String DOWNLOAD_PROJECT_SOURCE = "project-source"; 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} * Relative path within {@link com.google.appinventor.server.DownloadServlet}
* for downloading all of a user's projects' sources. * 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