+ if (executorService != null && !executorService.isTerminated())
+ throw new IllegalStateException("Another backup is running");
+ executorService = Executors.newFixedThreadPool(threadCount);
+ long begin = System.currentTimeMillis();
+ // software backup
+ if (bundleContext != null && performSoftwareBackup)
+ executorService.submit(() -> performSoftwareBackup(bundleContext));
+
+ // data backup
+ Session defaultSession = login(null);
+ defaultWorkspace = defaultSession.getWorkspace().getName();
+ try {
+ String[] workspaceNames = defaultSession.getWorkspace().getAccessibleWorkspaceNames();
+ workspaces: for (String workspaceName : workspaceNames) {
+ if ("security".equals(workspaceName))
+ continue workspaces;
+ performDataBackup(workspaceName);
+ }
+ } finally {
+ JcrUtils.logoutQuietly(defaultSession);
+ executorService.shutdown();
+ try {
+ executorService.awaitTermination(24, TimeUnit.HOURS);
+ } catch (InterruptedException e) {
+ // silent
+ throw new IllegalStateException("Backup was interrupted before completion", e);
+ }
+ }
+ // versions
+ executorService = Executors.newFixedThreadPool(threadCount);
+ try {
+ performVersionsBackup();
+ } finally {
+ executorService.shutdown();
+ try {
+ executorService.awaitTermination(24, TimeUnit.HOURS);
+ } catch (InterruptedException e) {
+ // silent
+ throw new IllegalStateException("Backup was interrupted before completion", e);
+ }
+ }
+ long duration = System.currentTimeMillis() - begin;
+ if (isBackupFailed())
+ log.info("System logical backup failed after " + (duration / 60000) + "min " + (duration / 1000) + "s");
+ else
+ log.info("System logical backup completed in " + (duration / 60000) + "min " + (duration / 1000) + "s");
+ }
+
+ protected void performDataBackup(String workspaceName) throws RepositoryException, IOException {
+ Session session = login(workspaceName);
+ try {
+ nodes: for (NodeIterator nit = session.getRootNode().getNodes(); nit.hasNext();) {
+ if (isBackupFailed())
+ return;
+ Node nodeToExport = nit.nextNode();
+ if (JCR_SYSTEM.equals(nodeToExport.getName()))
+ continue nodes;
+ String nodePath = nodeToExport.getPath();
+ Future<Set<String>> contentPathsFuture = executorService
+ .submit(() -> performNodeBackup(workspaceName, nodePath));
+ executorService.submit(() -> performFilesBackup(workspaceName, contentPathsFuture));
+ }
+ } finally {
+ Jcr.logout(session);
+ }
+ }
+
+ protected void performVersionsBackup() throws RepositoryException, IOException {
+ Session session = login(defaultWorkspace);
+ Node versionStorageNode = session.getNode(JCR_VERSION_STORAGE_PATH);
+ try {
+ for (NodeIterator nit = versionStorageNode.getNodes(); nit.hasNext();) {
+ Node nodeToExport = nit.nextNode();
+ String nodePath = nodeToExport.getPath();
+ if (isBackupFailed())
+ return;
+ Future<Set<String>> contentPathsFuture = executorService
+ .submit(() -> performNodeBackup(defaultWorkspace, nodePath));
+ executorService.submit(() -> performFilesBackup(defaultWorkspace, contentPathsFuture));
+ }
+ } finally {
+ Jcr.logout(session);
+ }
+
+ }
+
+ protected Set<String> performNodeBackup(String workspaceName, String nodePath) {
+ Session session = login(workspaceName);
+ try {
+ Node nodeToExport = session.getNode(nodePath);
+// String nodeName = nodeToExport.getName();
+// if (nodeName.startsWith("jcr:") || nodeName.startsWith("rep:"))
+// continue nodes;
+// // TODO make it more robust / configurable
+// if (nodeName.equals("user"))
+// continue nodes;
+ String relativePath = WORKSPACES_BASE + workspaceName + nodePath + ".xml";
+ OutputStream xmlOut = openOutputStream(relativePath);
+ BackupContentHandler contentHandler;
+ try (Writer writer = new BufferedWriter(new OutputStreamWriter(xmlOut, StandardCharsets.UTF_8))) {
+ contentHandler = new BackupContentHandler(writer, nodeToExport);
+ session.exportSystemView(nodeToExport.getPath(), contentHandler, true, false);
+ if (log.isDebugEnabled())
+ log.debug(workspaceName + ":" + nodePath + " metadata exported to " + relativePath);
+ }
+
+ // Files
+ Set<String> contentPaths = contentHandler.getContentPaths();
+ return contentPaths;
+ } catch (Exception e) {
+ markBackupFailed("Cannot backup node " + workspaceName + ":" + nodePath, e);
+ throw new ThreadDeath();
+ } finally {
+ Jcr.logout(session);
+ }
+ }
+
+ protected void performFilesBackup(String workspaceName, Future<Set<String>> contentPathsFuture) {
+ Set<String> contentPaths;
+ try {
+ contentPaths = contentPathsFuture.get(24, TimeUnit.HOURS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e1) {
+ markBackupFailed("Cannot retrieve content paths for workspace " + workspaceName, e1);
+ return;
+ }
+ if (contentPaths == null || contentPaths.size() == 0)
+ return;
+ Session session = login(workspaceName);
+ try {
+ String workspacesFilesBasePath = FILES_BASE + workspaceName;
+ for (String path : contentPaths) {
+ if (isBackupFailed())
+ return;
+ Node contentNode = session.getNode(path);
+ Binary binary = null;
+ try {
+ binary = contentNode.getProperty(Property.JCR_DATA).getBinary();
+ String fileRelativePath = workspacesFilesBasePath + contentNode.getParent().getPath();
+
+ // checksum
+ boolean skip = false;
+ String checksum = null;
+ if (session instanceof JackrabbitSession) {
+ JackrabbitValue value = (JackrabbitValue) contentNode.getProperty(Property.JCR_DATA).getValue();
+// ReferenceBinary referenceBinary = (ReferenceBinary) binary;
+ checksum = value.getContentIdentity();
+ }
+ if (checksum != null) {
+ if (!checksums.containsKey(checksum)) {
+ checksums.put(checksum, fileRelativePath);
+ } else {
+ skip = true;
+ String sourcePath = checksums.get(checksum);
+ if (log.isTraceEnabled())
+ log.trace(fileRelativePath + " : already " + sourcePath + " with checksum " + checksum);
+ createLink(sourcePath, fileRelativePath);
+ try (Writer writerSum = new OutputStreamWriter(
+ openOutputStream(fileRelativePath + ".sha256"), StandardCharsets.UTF_8)) {
+ writerSum.write(checksum);
+ }
+ }
+ }
+
+ // copy file
+ if (!skip)
+ try (InputStream in = binary.getStream();
+ OutputStream out = openOutputStream(fileRelativePath)) {
+ IOUtils.copy(in, out);
+ if (log.isTraceEnabled())
+ log.trace("Workspace " + workspaceName + ": file content exported to "
+ + fileRelativePath);
+ }
+ } finally {
+ JcrUtils.closeQuietly(binary);
+ }
+ }
+ if (log.isDebugEnabled())
+ log.debug(workspaceName + ":" + contentPaths.size() + " files exported to " + workspacesFilesBasePath);
+ } catch (Exception e) {
+ markBackupFailed("Cannot backup files from " + workspaceName + ":", e);
+ } finally {
+ Jcr.logout(session);
+ }
+ }
+
+ protected OutputStream openOutputStream(String relativePath) throws IOException {
+ if (zout != null) {
+ ZipEntry entry = new ZipEntry(relativePath);
+ zout.putNextEntry(entry);
+ return zout;
+ } else if (basePath != null) {
+ Path targetPath = basePath.resolve(Paths.get(relativePath));
+ Files.createDirectories(targetPath.getParent());
+ return Files.newOutputStream(targetPath);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ protected void createLink(String source, String target) throws IOException {
+ if (zout != null) {
+ // TODO implement for zip
+ throw new UnsupportedOperationException();
+ } else if (basePath != null) {
+ Path sourcePath = basePath.resolve(Paths.get(source));
+ Path targetPath = basePath.resolve(Paths.get(target));
+ Path relativeSource = targetPath.getParent().relativize(sourcePath);
+ Files.createDirectories(targetPath.getParent());
+ Files.createSymbolicLink(targetPath, relativeSource);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ protected void closeOutputStream(String relativePath, OutputStream out) throws IOException {
+ if (zout != null) {
+ zout.closeEntry();
+ } else if (basePath != null) {
+ out.close();
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ protected Session login(String workspaceName) {
+ if (bundleContext != null) {// local
+ return NodeUtils.openDataAdminSession(repository, workspaceName);
+ } else {// remote
+ try {
+ return repository.login(workspaceName);
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+ }
+
+ public final static void main(String[] args) throws Exception {
+ if (args.length == 0) {
+ printUsage("No argument");
+ System.exit(1);
+ }
+ URI uri = new URI(args[0]);
+ Repository repository = createRemoteRepository(uri);
+ Path basePath = args.length > 1 ? Paths.get(args[1]) : Paths.get(System.getProperty("user.dir"));
+ if (!Files.exists(basePath))
+ Files.createDirectories(basePath);
+ LogicalBackup backup = new LogicalBackup(null, repository, basePath);
+ backup.run();
+ }
+
+ private static void printUsage(String errorMessage) {
+ if (errorMessage != null)
+ System.err.println(errorMessage);
+ System.out.println("Usage: LogicalBackup <remote URL> [<target directory>]");
+
+ }
+
+ protected static Repository createRemoteRepository(URI uri) throws RepositoryException {
+ RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory();
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString());
+ // TODO make it configurable
+ params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, NodeConstants.SYS_WORKSPACE);
+ return repositoryFactory.getRepository(params);
+ }
+
+ public void performSoftwareBackup(BundleContext bundleContext) {
+ String bootBasePath = OSGI_BASE + "boot";
+ Bundle[] bundles = bundleContext.getBundles();
+ for (Bundle bundle : bundles) {
+ String relativePath = bootBasePath + "/" + bundle.getSymbolicName() + ".jar";