Groovy Script For XAR Export

I’d like to XAR-export my top-level pages on a regular basis using the scheduler, but I fail to access the URL with Groovy. While e.g. the URL

http://dew7d00pc014943:8080/xwiki/bin/export/Test/WebHome/?format=xar&name=Test.2018-12-04&pages=xwiki%3ATest.%25&history=true&backup=true

works well when accessed with the browser, in Groovy

import java.net.URL
String url = http://myhost:8080/xwiki/bin/export/Team/WebHome/?format=xar&name=Team.2018-12-04&pages=xwiki%3ATeam.%25&history=true&backup=true
new URL(url).getText()

results in

Caused by: java.io.FileNotFoundException: http://myhost:8080/xwiki/bin/export/Test/WebHome/?format=xar&name=Test.2018-12-04&pages=xwiki%3ATest.%25&history=true&backup=true
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(Unknown Source) 
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream(Unknown Source)

What am I missing?

Thanks
Guido

Going trough a URL from a script is usually asking for trouble. The best is generally to do what this service does behind the scene, here directly manipulate the Filter API to “convert” and instance into a XAR file.

To help with that there is a script.instance script service which contains a few helpers. You can find a (Velocity) example on https://extensions.xwiki.org/xwiki/bin/view/Extension/XWiki%20Platform%20-%20Filter%20-%20Instance%20-%20Script/.

Thank you, Thomas.

I’ve tried to set up a groovy script following your velocity example:

import org.xwiki.filter.instance.input.InstanceInputProperties
import org.xwiki.filter.type.FilterStreamType
import org.xwiki.logging.LogLevel
import org.xwiki.job.Job
import org.xwiki.model.reference.DocumentReference
import org.xwiki.model.reference.EntityReferenceSet

String page = "Main.WebHome"
EntityReferenceSet entities = services.filter.instance.newEntityReferenceSet()
entities.includes(new DocumentReference("wiki", "xwiki", page))
InstanceInputProperties iProps = services.filter.instance.newInstanceInputProperties()
String target = "C:/Temp/${page}.xar"
Map oProps = [target: target]
Job job = services.filter.instance.startExport(FilterStreamType.XWIKI_XAR_CURRENT, oProps, iProps)
job.join()
for (event in job.getStatus().getLog().getLogsFrom(LogLevel.DEBUG)) {
    println "{{{$event}}}"
}

but that fails:

INFO:Starting job of type [filter.converter] with identifier [[filter, converter, xwiki+instance, xwiki+xar/1.3]]
ERROR:Exception thrown during job execution
INFO:Finished job of type [filter.converter] with identifier [[filter, converter, xwiki+instance, xwiki+xar/1.3]]

Can you spot where I failed?

I don’t think this does what you expect it to do: this is a reference to the page wiki:xwiki.Main\.WebHome.

If what you want is the reference of the wiki (like the Velocity example does) then it’s new WikiReference('xwiki'). The wiki: prefix in the Velocity example indicate the type of the reference (a wiki reference here and not a document reference). If what you want is only to export the home page then it’s new DocumentReference('xwiki', 'Main', 'WebHome').

target is actually a very generic parameter (could be an OutputStream, etc.) so when you pass a file path you need to make is explicit by prefixing the value with ‘file:’ like in the Velocity example does (which is the “target type”).

Okay, got it. Since I want to backup the top-level spaces the following works for me (actually, I did not set the entities before):

EntityReferenceSet entities = services.filter.instance.newEntityReferenceSet()
entities.includes(new SpaceReference("TopLevelSpaceName", new WikiReference("xwiki")))
InstanceInputProperties iProps = services.filter.instance.newInstanceInputProperties()
iProps.setVerbose(true)
iProps.setEntities(entities)
File out = new File("C:/Temp/TopLevelSpaceName.xar")
Map oProps = [target: out]
Job job = services.filter.instance.startExport(FilterStreamType.XWIKI_XAR_CURRENT, oProps, iProps)
job.join()

I’d still like do be also able to manual trigger a download on the client side. I could generate download links and fiddle with JavaScript to invoke them one after the other. I’ll see.

Thanks for your help. Cheers, Guido

As I indicated in the previous message, target can be many things, including an OutputStream. That means you can pass the HTTP response OutputStream to directly send back the content to the HTTP client. A good example of that is the export action itself: xwiki-platform/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/ExportAction.java at master · xwiki/xwiki-platform · GitHub and xwiki-platform/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/ExportAction.java at master · xwiki/xwiki-platform · GitHub. But this use case is also covered by directly calling the export action URL from the client.

Thank you very much, Thomas!

Here is the complete working example: The content of a page that allows to download or backup a user selection of top level spaces and lists download links for all top level spaces

{{groovy}}
import com.xpn.xwiki.api.Document
import com.xpn.xwiki.util.Util;
import com.xpn.xwiki.web.XWikiServletResponse
import org.xwiki.logging.LogLevel
import org.xwiki.job.Job
import java.text.DateFormat
import java.text.SimpleDateFormat
import org.xwiki.filter.instance.input.DocumentInstanceInputProperties
import org.xwiki.filter.output.DefaultOutputStreamOutputTarget;
import org.xwiki.filter.type.FilterStreamType
import org.xwiki.model.reference.EntityReferenceSet
import org.xwiki.model.reference.SpaceReference
import org.xwiki.model.reference.WikiReference

TimeZone tz = TimeZone.getTimeZone("UTC")
DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss")
df.setTimeZone(tz)
String now = df.format(new Date())

ArrayList<String> exclude = ["Help", "Menu", "XWiki", "Sandbox", "ScriptingDocumentation"]
ArrayList<String> names = new ArrayList<>()
Document home = xwiki.getDocument("Main.WebHome")
for (name in home.getChildren()) {
    name = name.tokenize(".")[0]
    if (!exclude.contains(name)) {
        names << name
    }
}
names.sort()

String[] pages = request.getParameterValues("pages")

StringBuffer out = new StringBuffer()
out << """
(% class="row" %)
(((
(% class="col-md-4" %)
(((
{{html}}
<form action="" id="backup" method="post">
  <div>
    <table>
      <tbody>
        <tr>
          <th>Pages</th>
          <td>
            <select name="pages" multiple size=${names.size()}>
"""
for (name in names) {
    out << "<option"
    if (pages == null || pages.contains(name)) {
        out << " selected"
    }
    out << ">" << name << "</option>\n"
}
out << """
            </select>
          </td>
        </tr>
        <tr>
          <th>Target</th>
          <td>
            <select name="target" size=1 onchange="setButtonText(this.value)">
"""
for (opt in ["Server", "Download"]) {
    out << "<option"
    if (request.target == opt) {
        out << " selected"
    }
    out << ">" << opt << "</option>\n"
}
out << """
            </select>
          </td>
        </tr>
      </tbody>
    </table>
    <span class="buttonwrapper"><input id="btn1" type="submit" value="Backup Selected Pages" class="button"/></span>
  </div>
</form>
<script type="text/javascript">
function setButtonText(val) {
    if (val == "Server") {
        document.getElementById("btn1").value="Backup Selected Pages"
    } else {
        document.getElementById("btn1").value="Download Selected Pages"
    }
}
</script>
{{/html}}

)))
(% class="col-md-4" %)
(((

"""
String base = xcontext.URLFactory.getServerURL(xcontext.context)
for (name in names) {
  String url = "${base}/xwiki/bin/export/${name}/WebHome/?format=xar&name=${name}.${now}&pages=xwiki%3A${name}.%25&history=true&backup=true"
    out << "\n[[Download ${name}>>${url}]]"
}
out << """
)))
)))


"""
if (pages != null) {
    EntityReferenceSet entities = new EntityReferenceSet()
    for (page in pages) {
        entities.includes(new SpaceReference(page, new WikiReference("xwiki")))
    }
    DocumentInstanceInputProperties iProps = new DocumentInstanceInputProperties()
    iProps.setEntities(entities)
    iProps.setVerbose(true)
    iProps.setWithJRCSRevisions(true)
    iProps.setWithRevisions(false)
    Map oProps = [
        packageBackupPack: Boolean.TRUE
    ]
    String filename = "Backup.${now}.xar"
    if (request.target == "Server") {
        oProps["target"] = new File("/var/local/xwiki/${filename}")
    } else {
        filename = Util.encodeURI(filename, xcontext.getContext())
        XWikiServletResponse response = xcontext.getResponse()
        response.setContentType("application/zip")
        response.setHeader("Content-disposition", "attachment; filename=${filename}")
        oProps["target"] = response.getOutputStream()
    }
    Job job = services.filter.instance.startExport(FilterStreamType.XWIKI_XAR_CURRENT, oProps, iProps)
    job.join()
    for (event in job.getStatus().getLog().getLogsFrom(LogLevel.DEBUG)) {
        out << "{{{$event}}}" << "\n"
    }
}

print out.toString()
{{/groovy}}

A little remark: This version generates duplicate backup requests if the selected space contains subspaces.
Changing line 30 from “names.sort()” to “names.sort().unique()” helps.

Norbert