Skip to content

Commit 1f3700f

Browse files
authored
CLDR-18837 Button to download generated VXML in a zip (#5021)
1 parent dfe36c2 commit 1f3700f

File tree

5 files changed

+198
-3
lines changed

5 files changed

+198
-3
lines changed

tools/cldr-apps/js/src/esm/cldrGenerateVxml.mjs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const SECONDS_IN_MS = 1000;
1010

1111
const REQUEST_TIMER = 5 * SECONDS_IN_MS; // Fetch status this often
1212

13-
const VXML_URL = "api/vxml";
13+
const VXML_URL = "vxml";
14+
const VXML_DOWNLOAD_URL = "vxml/download";
1415

1516
// These must match the back end; used in requests
1617
class RequestType {
@@ -62,9 +63,10 @@ function cancel() {
6263

6364
function requestVxml(requestType) {
6465
const args = { requestType: requestType };
66+
const url = cldrAjax.makeApiUrl(VXML_URL, null);
6567
const init = cldrAjax.makePostData(args);
6668
cldrAjax
67-
.doFetch(VXML_URL, init)
69+
.doFetch(url, init)
6870
.then(cldrAjax.handleFetchErrors)
6971
.then((r) => r.json())
7072
.then(setVxmlData)
@@ -73,6 +75,28 @@ function requestVxml(requestType) {
7375
});
7476
}
7577

78+
async function download(directory) {
79+
const dirName = directory.split("/").pop(); // only the part after slash
80+
const p = new URLSearchParams();
81+
p.append("dirName", dirName);
82+
const url = cldrAjax.makeApiUrl(VXML_DOWNLOAD_URL, p);
83+
try {
84+
const response = await cldrAjax.doFetch(url);
85+
if (!response.ok) {
86+
throw new Error(response.statusText);
87+
}
88+
const bytes = await response.bytes();
89+
const a = document.createElement("a");
90+
a.href = window.URL.createObjectURL(
91+
new Blob([bytes], { type: "application/octet-stream" })
92+
);
93+
a.download = dirName + ".zip";
94+
a.click();
95+
} catch (e) {
96+
cldrNotify.exception(e, "Downloading VXML");
97+
}
98+
}
99+
76100
function setVxmlData(data) {
77101
if (!callbackToSetData) {
78102
return;
@@ -88,4 +112,4 @@ function setVxmlData(data) {
88112
}
89113
}
90114

91-
export { Status, cancel, canGenerateVxml, start, viewMounted };
115+
export { Status, cancel, canGenerateVxml, download, start, viewMounted };

tools/cldr-apps/js/src/views/GenerateVxml.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ function cancel() {
4444
status.value = STATUS.STOPPED;
4545
}
4646
47+
function download() {
48+
cldrGenerateVxml.download(directory.value);
49+
}
50+
4751
function canCancel() {
4852
return status.value === STATUS.WAITING || status.value === STATUS.PROCESSING;
4953
}
@@ -100,6 +104,15 @@ defineExpose({
100104
<span>Directory created: {{ directory }}</span>
101105
&nbsp;
102106
<button @click="copyDirectory()">Copy</button>
107+
&nbsp;
108+
<!-- Allow downloading even if STATUS.STOPPED. If generation or verification failed,
109+
downloading may still help with diagnosing partial/problematic output. -->
110+
<button
111+
v-if="status == STATUS.SUCCEEDED || status == STATUS.STOPPED"
112+
@click="download()"
113+
>
114+
Download
115+
</button>
103116
</p>
104117
<p v-if="message">{{ message }}</p>
105118
<p v-if="localeId">

tools/cldr-apps/src/main/java/org/unicode/cldr/web/OutputFileManager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ private boolean outputAllFiles(VxmlGenerator vxmlGenerator, VxmlQueue.Results re
213213
}
214214

215215
public static Set<CLDRLocale> createVxmlLocaleSet() {
216+
if (false) { // Debugging only, to save time! Test with aa only
217+
Set<CLDRLocale> set = new TreeSet<>();
218+
set.add(CLDRLocale.getInstance("aa"));
219+
return set;
220+
}
216221
Set<CLDRLocale> set = new TreeSet<>(SurveyMain.getLocalesSet());
217222
// skip "en" and "root", since they should never be changed by the Survey Tool
218223
set.remove(CLDRLocale.getInstance("en"));

tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/GenerateVxml.java

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.unicode.cldr.web.api;
22

3+
import java.io.File;
34
import java.io.IOException;
45
import javax.enterprise.context.ApplicationScoped;
56
import javax.ws.rs.*;
@@ -12,6 +13,7 @@
1213
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
1314
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
1415
import org.unicode.cldr.util.CLDRLocale;
16+
import org.unicode.cldr.util.Zipper;
1517
import org.unicode.cldr.web.*;
1618

1719
@ApplicationScoped
@@ -134,4 +136,89 @@ public static final class VxmlResponse {
134136
@Schema(description = "Verification warning messages")
135137
public String[] verificationWarnings;
136138
}
139+
140+
@GET
141+
@Path("/download")
142+
@Produces(MediaType.APPLICATION_OCTET_STREAM)
143+
@Operation(summary = "Download VXML", description = "Download VXML as zip file")
144+
@APIResponses(
145+
value = {
146+
@APIResponse(
147+
responseCode = "200",
148+
description = "Download VXML",
149+
content =
150+
@Content(
151+
mediaType = "application/zip",
152+
schema = @Schema(implementation = Response.class))),
153+
@APIResponse(responseCode = "403", description = "Forbidden"),
154+
@APIResponse(responseCode = "404", description = "Not Found"),
155+
@APIResponse(
156+
responseCode = "500",
157+
description = "Internal Server Error",
158+
content =
159+
@Content(
160+
mediaType = "application/json",
161+
schema = @Schema(implementation = STError.class))),
162+
})
163+
public Response downloadVxml(
164+
@QueryParam("dirName")
165+
@Schema(
166+
description =
167+
"The name (not full path) of the directory containing VXML")
168+
String dirName,
169+
@HeaderParam(Auth.SESSION_HEADER) String sessionString) {
170+
try {
171+
CookieSession cs = Auth.getSession(sessionString);
172+
if (cs == null) {
173+
return Auth.noSessionResponse();
174+
}
175+
if (!UserRegistry.userIsTCOrStronger(cs.user)) {
176+
return Response.status(Response.Status.FORBIDDEN).build();
177+
}
178+
if (SurveyMain.isBusted()
179+
|| !SurveyMain.wasInitCalled()
180+
|| !SurveyMain.triedToStartUp()) {
181+
return STError.surveyNotQuiteReady();
182+
}
183+
cs.userDidAction();
184+
File dir = getDirectory(dirName);
185+
if (dir == null) {
186+
return Response.status(Response.Status.NOT_FOUND).build();
187+
}
188+
File zipFile = Zipper.zipDirectory(dir);
189+
return Response.ok(zipFile).header("Content-Type", "application/zip").build();
190+
} catch (Exception e) {
191+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(e).build();
192+
}
193+
}
194+
195+
/**
196+
* Sanitize the "user-provided" dirName, normally actually provided by the back end, but
197+
* possibly concocted by evildoers. It should contain no slashes or periods, and should match
198+
* one of the "vetdata-..." siblings of the automatic vetdata dir CookieSession.sm.getVetdir().
199+
* Some of this sanitizing is redundant and is really intended to satisfy automatic checkers and
200+
* prevent failures like "Uncontrolled data used in path expression".
201+
*
202+
* @param dirName the directory name (not full path)
203+
* @return the directory (sibling of the automatic vetdata directory)
204+
*/
205+
private File getDirectory(String dirName) {
206+
if (dirName.contains("/") || dirName.contains(".")) {
207+
return null;
208+
}
209+
File autoVetDir = CookieSession.sm.getVetdir();
210+
if (!dirName.startsWith(autoVetDir.getName())) {
211+
return null;
212+
}
213+
File parent = autoVetDir.getParentFile();
214+
File[] children = parent.listFiles();
215+
if (children != null) {
216+
for (File child : children) {
217+
if (dirName.equals(child.getName())) {
218+
return child;
219+
}
220+
}
221+
}
222+
return null;
223+
}
137224
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.unicode.cldr.util;
2+
3+
import java.io.File;
4+
import java.io.FileInputStream;
5+
import java.io.FileOutputStream;
6+
import java.io.IOException;
7+
import java.util.zip.ZipEntry;
8+
import java.util.zip.ZipOutputStream;
9+
10+
public class Zipper {
11+
12+
private static final int BUFFER_SIZE = 4096;
13+
14+
/**
15+
* Create a zip file from the given directory.
16+
*
17+
* <p>If the directory is /foo/bar, then the zip file will be /foo/bar.zip
18+
*
19+
* @param dir the given directory
20+
* @return the zip file
21+
* @throws IOException if unable to read/write files
22+
*/
23+
public static File zipDirectory(File dir) throws IOException {
24+
String zipFileName = dir.getAbsolutePath() + ".zip";
25+
FileOutputStream fos = new FileOutputStream(zipFileName);
26+
ZipOutputStream zos = new ZipOutputStream(fos);
27+
zipFile(dir, dir.getName(), zos);
28+
zos.close();
29+
fos.close();
30+
return new File(zipFileName);
31+
}
32+
33+
/**
34+
* Create a zip file from the given file or directory
35+
*
36+
* <p>Note: this function calls itself recursively
37+
*
38+
* @param file the file or directory to zip
39+
* @param name the name of the file, including its slash-separated path when called recursively
40+
* @param zos the ZipOutputStream
41+
* @throws IOException if unable to read/write files
42+
*/
43+
private static void zipFile(File file, String name, ZipOutputStream zos) throws IOException {
44+
if (file.isDirectory()) {
45+
zos.putNextEntry(new ZipEntry(name.endsWith("/") ? name : name + "/"));
46+
zos.closeEntry();
47+
File[] children = file.listFiles();
48+
if (children != null) {
49+
for (File child : children) {
50+
if (!child.isHidden()) {
51+
zipFile(child, name + "/" + child.getName(), zos);
52+
}
53+
}
54+
}
55+
} else {
56+
FileInputStream fis = new FileInputStream(file);
57+
zos.putNextEntry(new ZipEntry(name));
58+
byte[] bytes = new byte[BUFFER_SIZE];
59+
int length;
60+
while ((length = fis.read(bytes)) >= 0) {
61+
zos.write(bytes, 0, length);
62+
}
63+
fis.close();
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)