Improve logical backups.
authorMathieu Baudier <mbaudier@argeo.org>
Tue, 19 Jan 2021 08:31:55 +0000 (09:31 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Tue, 19 Jan 2021 08:31:55 +0000 (09:31 +0100)
org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsDeployment.java
org.argeo.cms/src/org/argeo/cms/internal/kernel/DeployConfig.java
org.argeo.maintenance/src/org/argeo/maintenance/backup/BackupContentHandler.java
org.argeo.maintenance/src/org/argeo/maintenance/backup/LogicalBackup.java
org.argeo.maintenance/src/org/argeo/maintenance/backup/LogicalRestore.java

index 57ce898097c61cfe05c14068e684fd0bad447a3f..2667d986a93efc8c737a909ed0c1941c0e8f80af 100644 (file)
@@ -295,13 +295,15 @@ public class CmsDeployment implements NodeDeployment {
                prepareDataModel(NodeConstants.NODE_REPOSITORY, deployedNodeRepository, publishAsLocalRepo);
 
                // init from backup
-               Path restorePath = Paths.get(System.getProperty("user.dir"), "restore");
-               if (Files.exists(restorePath)) {
-                       if (log.isDebugEnabled())
-                               log.debug("Found backup " + restorePath + ", restoring it...");
-                       LogicalRestore logicalRestore = new LogicalRestore(bc, deployedNodeRepository, restorePath);
-                       KernelUtils.doAsDataAdmin(logicalRestore);
-                       log.info("Restored backup from " + restorePath);
+               if (deployConfig.isFirstInit()) {
+                       Path restorePath = Paths.get(System.getProperty("user.dir"), "restore");
+                       if (Files.exists(restorePath)) {
+                               if (log.isDebugEnabled())
+                                       log.debug("Found backup " + restorePath + ", restoring it...");
+                               LogicalRestore logicalRestore = new LogicalRestore(bc, deployedNodeRepository, restorePath);
+                               KernelUtils.doAsDataAdmin(logicalRestore);
+                               log.info("Restored backup from " + restorePath);
+                       }
                }
 
                // init from repository
index 0a7d26584d96b14538c2f69309a51f30cc80e498..228ccbb4a172d2bacae5af3e3ea385064c68de92 100644 (file)
@@ -20,7 +20,6 @@ import javax.naming.ldap.Rdn;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.argeo.api.NodeConstants;
-import org.argeo.cms.CmsException;
 import org.argeo.naming.AttributesDictionary;
 import org.argeo.naming.LdifParser;
 import org.argeo.naming.LdifWriter;
@@ -42,19 +41,20 @@ class DeployConfig implements ConfigurationListener {
        private SortedMap<LdapName, Attributes> deployConfigs = new TreeMap<>();
        private final DataModels dataModels;
 
+       private boolean isFirstInit = false;
+
        public DeployConfig(ConfigurationAdmin configurationAdmin, DataModels dataModels, boolean isClean) {
                this.dataModels = dataModels;
                // ConfigurationAdmin configurationAdmin =
                // bc.getService(bc.getServiceReference(ConfigurationAdmin.class));
                try {
-                       boolean isFirstInit = false;
                        if (!isInitialized()) { // first init
                                isFirstInit = true;
                                firstInit();
                        }
                        init(configurationAdmin, isClean, isFirstInit);
                } catch (IOException e) {
-                       throw new CmsException("Could not init deploy configs", e);
+                       throw new RuntimeException("Could not init deploy configs", e);
                }
                // FIXME check race conditions during initialization
                // bc.registerService(ConfigurationListener.class, this, null);
@@ -108,15 +108,15 @@ class DeployConfig implements ConfigurationListener {
                        LdapName userAdminFactoryName = serviceFactoryDn(NodeConstants.NODE_USER_ADMIN_PID);
                        for (LdapName name : deployConfigs.keySet()) {
                                if (name.startsWith(userAdminFactoryName) && !name.equals(userAdminFactoryName)) {
-                                       try {
-                                               Attributes attrs = deployConfigs.get(name);
-                                               String cn = name.getRdn(name.size() - 1).getValue().toString();
-                                               if (!activeCns.contains(cn)) {
-                                                       attrs.put(UserAdminConf.disabled.name(), "true");
-                                               }
-                                       } catch (Exception e) {
-                                               throw new CmsException("Cannot disable user directory " + name, e);
+//                                     try {
+                                       Attributes attrs = deployConfigs.get(name);
+                                       String cn = name.getRdn(name.size() - 1).getValue().toString();
+                                       if (!activeCns.contains(cn)) {
+                                               attrs.put(UserAdminConf.disabled.name(), "true");
                                        }
+//                                     } catch (Exception e) {
+//                                             throw new CmsException("Cannot disable user directory " + name, e);
+//                                     }
                                }
                        }
                }
@@ -186,7 +186,7 @@ class DeployConfig implements ConfigurationListener {
                        deployConfigs = new LdifParser().read(in);
                }
                if (isClean) {
-                       if(log.isDebugEnabled())
+                       if (log.isDebugEnabled())
                                log.debug("Clean state, loading from framework properties...");
                        setFromFrameworkProperties(isFirstInit);
                        for (LdapName dn : deployConfigs.keySet()) {
@@ -343,8 +343,12 @@ class DeployConfig implements ConfigurationListener {
                        return null;
        }
 
-       static boolean isInitialized() {
+       private static boolean isInitialized() {
                return Files.exists(deployConfigPath);
        }
 
+       public boolean isFirstInit() {
+               return isFirstInit;
+       }
+
 }
index 745d39d1d36f9ad22fabf44bd5d2e8c41d6883bb..e29483e975e82e8ef752f3821cc41f2c2872b265 100644 (file)
@@ -39,7 +39,7 @@ public class BackupContentHandler extends DefaultHandler {
        private Session session;
        private Set<String> contentPaths = new TreeSet<>();
 
-       private boolean inSystem = false;
+//     private boolean inSystem = false;
 
        public BackupContentHandler(Writer out, Session session) {
                super();
@@ -75,15 +75,15 @@ public class BackupContentHandler extends DefaultHandler {
                if (isNode) {
                        String nodeName = attributes.getValue(SV_NAMESPACE_URI, NAME);
                        currentDepth = currentDepth + 1;
-                       if (currentDepth > 0)
-                               currentPath[currentDepth - 1] = nodeName;
+//                     if (currentDepth >= 0)
+                       currentPath[currentDepth] = nodeName;
 //                     System.out.println(getCurrentPath() + " , depth=" + currentDepth);
-                       if ("jcr:system".equals(nodeName)) {
-                               inSystem = true;
-                       }
+//                     if ("jcr:system".equals(nodeName)) {
+//                             inSystem = true;
+//                     }
                }
-               if (inSystem)
-                       return;
+//             if (inSystem)
+//                     return;
 
                if (SV_NAMESPACE_URI.equals(uri))
                        try {
@@ -143,20 +143,21 @@ public class BackupContentHandler extends DefaultHandler {
        public void endElement(String uri, String localName, String qName) throws SAXException {
                if (localName.equals(NODE)) {
 //                     System.out.println("endElement " + getCurrentPath() + " , depth=" + currentDepth);
-                       if (currentDepth > 0)
-                               currentPath[currentDepth - 1] = null;
+//                     if (currentDepth > 0)
+                       currentPath[currentDepth] = null;
                        currentDepth = currentDepth - 1;
-                       if (inSystem) {
-                               // System.out.println("Skip " + getCurrentPath()+" ,
-                               // currentDepth="+currentDepth);
-                               if (currentDepth == 0) {
-                                       inSystem = false;
-                                       return;
-                               }
-                       }
+                       assert currentDepth >= 0;
+//                     if (inSystem) {
+//                             // System.out.println("Skip " + getCurrentPath()+" ,
+//                             // currentDepth="+currentDepth);
+//                             if (currentDepth == 0) {
+//                                     inSystem = false;
+//                                     return;
+//                             }
+//                     }
                }
-               if (inSystem)
-                       return;
+//             if (inSystem)
+//                     return;
                boolean isValue = localName.equals(VALUE);
                if (SV_NAMESPACE_URI.equals(uri))
                        try {
@@ -180,8 +181,8 @@ public class BackupContentHandler extends DefaultHandler {
 
        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
-               if (inSystem)
-                       return;
+//             if (inSystem)
+//                     return;
                try {
                        out.write(ch, start, length);
                } catch (IOException e) {
@@ -191,17 +192,17 @@ public class BackupContentHandler extends DefaultHandler {
 
        protected String getCurrentName() {
                assert currentDepth >= 0;
-               if (currentDepth == 0)
-                       return "jcr:root";
-               return currentPath[currentDepth - 1];
+//             if (currentDepth == 0)
+//                     return "jcr:root";
+               return currentPath[currentDepth];
        }
 
        protected String getCurrentPath() {
-               if (currentDepth == 0)
-                       return "/";
-               StringBuilder sb = new StringBuilder("/");
-               for (int i = 0; i < currentDepth; i++) {
-                       if (i != 0)
+//             if (currentDepth == 0)
+//                     return "/";
+               StringBuilder sb = new StringBuilder();
+               for (int i = 0; i <= currentDepth; i++) {
+//                     if (i != 0)
                                sb.append('/');
                        sb.append(currentPath[i]);
                }
index 864de25be9b3fa4483bb9aee8837f15149a0c664..4f7a2cfea561e7806a867a69268b9dcd2921b86f 100644 (file)
@@ -17,6 +17,13 @@ import java.util.Dictionary;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.jar.JarOutputStream;
 import java.util.jar.Manifest;
 import java.util.zip.ZipEntry;
@@ -25,6 +32,7 @@ import java.util.zip.ZipOutputStream;
 
 import javax.jcr.Binary;
 import javax.jcr.Node;
+import javax.jcr.NodeIterator;
 import javax.jcr.Property;
 import javax.jcr.Repository;
 import javax.jcr.RepositoryException;
@@ -37,6 +45,7 @@ import org.apache.commons.logging.LogFactory;
 import org.argeo.api.NodeConstants;
 import org.argeo.api.NodeUtils;
 import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory;
+import org.argeo.jcr.Jcr;
 import org.argeo.jcr.JcrException;
 import org.argeo.jcr.JcrUtils;
 import org.osgi.framework.Bundle;
@@ -53,26 +62,25 @@ public class LogicalBackup implements Runnable {
        private final static Log log = LogFactory.getLog(LogicalBackup.class);
 
        public final static String WORKSPACES_BASE = "workspaces/";
+       public final static String FILES_BASE = "files/";
        public final static String OSGI_BASE = "share/osgi/";
        private final Repository repository;
+       private String defaultWorkspace;
        private final BundleContext bundleContext;
 
        private final ZipOutputStream zout;
        private final Path basePath;
 
+       private ExecutorService executorService;
+
        public LogicalBackup(BundleContext bundleContext, Repository repository, Path basePath) {
                this.repository = repository;
                this.zout = null;
                this.basePath = basePath;
                this.bundleContext = bundleContext;
-       }
 
-//     public LogicalBackup(BundleContext bundleContext, Repository repository, ZipOutputStream zout) {
-//     this.repository = repository;
-//     this.zout = zout;
-//     this.basePath = null;
-//     this.bundleContext = bundleContext;
-//}
+               executorService = Executors.newFixedThreadPool(3);
+       }
 
        @Override
        public void run() {
@@ -80,143 +88,122 @@ public class LogicalBackup implements Runnable {
                        log.info("Start logical backup to " + basePath);
                        perform();
                } catch (Exception e) {
-                       e.printStackTrace();
+                       log.error("Unexpected exception when performing logical backup", e);
                        throw new IllegalStateException("Logical backup failed", e);
                }
 
        }
 
        public void perform() throws RepositoryException, IOException {
+               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;
-                               perform(workspaceName);
+                               performDataBackup(workspaceName);
                        }
                } finally {
                        JcrUtils.logoutQuietly(defaultSession);
+                       executorService.shutdown();
+                       try {
+                               executorService.awaitTermination(24, TimeUnit.HOURS);
+                       } catch (InterruptedException e) {
+                               // silent
+                       }
                }
-
+               long duration = System.currentTimeMillis() - begin;
+               log.info("System logical backup completed in " + (duration / 60000) + "min " + (duration / 1000) + "s");
        }
 
-       public void performSoftwareBackup() throws IOException {
-               for (Bundle bundle : bundleContext.getBundles()) {
-                       String relativePath = OSGI_BASE + "boot/" + bundle.getSymbolicName() + ".jar";
-                       Dictionary<String, String> headers = bundle.getHeaders();
-                       Manifest manifest = new Manifest();
-                       Enumeration<String> headerKeys = headers.keys();
-                       while (headerKeys.hasMoreElements()) {
-                               String headerKey = headerKeys.nextElement();
-                               String headerValue = headers.get(headerKey);
-                               manifest.getMainAttributes().putValue(headerKey, headerValue);
-                       }
-                       try (JarOutputStream jarOut = new JarOutputStream(openOutputStream(relativePath), manifest)) {
-//                             Enumeration<String> entryPaths = bundle.getEntryPaths("/");
-//                             while (entryPaths.hasMoreElements()) {
-//                                     String entryPath = entryPaths.nextElement();
-//                                     ZipEntry entry = new ZipEntry(entryPath);
-//                                     URL entryUrl = bundle.getEntry(entryPath);
-//                                     try (InputStream in = entryUrl.openStream()) {
-//                                             jarOut.putNextEntry(entry);
-//                                             IOUtils.copy(in, jarOut);
-//                                             jarOut.closeEntry();
-//                                     } catch (FileNotFoundException e) {
-//                                             log.warn(entryPath);
-//                                     }
-//                             }
-                               Enumeration<URL> resourcePaths = bundle.findEntries("/", "*", true);
-                               resources: while (resourcePaths.hasMoreElements()) {
-                                       URL entryUrl = resourcePaths.nextElement();
-                                       String entryPath = entryUrl.getPath();
-                                       if (entryPath.equals(""))
-                                               continue resources;
-                                       if (entryPath.endsWith("/"))
-                                               continue resources;
-                                       String entryName = entryPath.substring(1);// remove first '/'
-                                       if (entryUrl.getPath().equals("/META-INF/"))
-                                               continue resources;
-                                       if (entryUrl.getPath().equals("/META-INF/MANIFEST.MF"))
-                                               continue resources;
-                                       // dev
-                                       if (entryUrl.getPath().startsWith("/target"))
-                                               continue resources;
-                                       if (entryUrl.getPath().startsWith("/src"))
-                                               continue resources;
-                                       if (entryUrl.getPath().startsWith("/ext"))
-                                               continue resources;
-
-                                       if (entryName.startsWith("bin/")) {// dev
-                                               entryName = entryName.substring("bin/".length());
-                                       }
-
-                                       ZipEntry entry = new ZipEntry(entryName);
-                                       try (InputStream in = entryUrl.openStream()) {
-                                               try {
-                                                       jarOut.putNextEntry(entry);
-                                               } catch (ZipException e) {// duplicate
-                                                       continue resources;
-                                               }
-                                               IOUtils.copy(in, jarOut);
-                                               jarOut.closeEntry();
-//                                             log.info(entryUrl);
-                                       } catch (FileNotFoundException e) {
-                                               log.warn(entryUrl + ": " + e.getMessage());
-                                       }
-                               }
+       protected void performDataBackup(String workspaceName) throws RepositoryException, IOException {
+               Session session = login(workspaceName);
+               try {
+                       nodes: for (NodeIterator nit = session.getRootNode().getNodes(); nit.hasNext();) {
+                               Node nodeToExport = nit.nextNode();
+                               if ("jcr:system".equals(nodeToExport.getName()) && !workspaceName.equals(defaultWorkspace))
+                                       continue nodes;
+                               String nodePath = nodeToExport.getPath();
+                               Future<Set<String>> contentPathsFuture = executorService
+                                               .submit(() -> performNodeBackup(workspaceName, nodePath));
+                               executorService.submit(() -> performFilesBackup(workspaceName, contentPathsFuture));
                        }
+               } finally {
+                       Jcr.logout(session);
                }
-
        }
 
-       public void perform(String workspaceName) throws RepositoryException, IOException {
+       protected Set<String> performNodeBackup(String workspaceName, String nodePath) {
                Session session = login(workspaceName);
                try {
-                       String relativePath = WORKSPACES_BASE + workspaceName + ".xml";
+                       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 + "/" + nodeName + ".xml";
                        OutputStream xmlOut = openOutputStream(relativePath);
                        BackupContentHandler contentHandler;
                        try (Writer writer = new BufferedWriter(new OutputStreamWriter(xmlOut, StandardCharsets.UTF_8))) {
                                contentHandler = new BackupContentHandler(writer, session);
-                               try {
-                                       session.exportSystemView("/", contentHandler, true, false);
-                                       if (log.isDebugEnabled())
-                                               log.debug("Workspace " + workspaceName + ": metadata exported to " + relativePath);
-                               } catch (SAXException e) {
-                                       throw new RuntimeException("Cannot perform backup of workspace " + workspaceName, e);
-                               } catch (RepositoryException e) {
-                                       throw new JcrException("Cannot perform backup of workspace " + workspaceName, e);
-                               }
+                               session.exportSystemView(nodeToExport.getPath(), contentHandler, true, false);
+                               if (log.isDebugEnabled())
+                                       log.debug(workspaceName + ":/" + nodeName + " metadata exported to " + relativePath);
                        }
-                       for (String path : contentHandler.getContentPaths()) {
+
+                       // Files
+                       Set<String> contentPaths = contentHandler.getContentPaths();
+                       return contentPaths;
+               } catch (IOException | SAXException e) {
+                       throw new RuntimeException("Cannot backup node " + workspaceName + ":" + nodePath, e);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot backup node " + workspaceName + ":" + nodePath, e);
+               } 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) {
+                       throw new RuntimeException("Cannot retrieve content paths for workspace " + workspaceName);
+               }
+               if (contentPaths == null || contentPaths.size() == 0)
+                       return;
+               Session session = login(workspaceName);
+               try {
+                       String workspacesFilesBasePath = FILES_BASE + workspaceName;
+                       for (String path : contentPaths) {
                                Node contentNode = session.getNode(path);
                                Binary binary = contentNode.getProperty(Property.JCR_DATA).getBinary();
-                               String fileRelativePath = WORKSPACES_BASE + workspaceName + contentNode.getParent().getPath();
+                               String fileRelativePath = workspacesFilesBasePath + contentNode.getParent().getPath();
                                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);
                                }
-
                        }
-
-//                     OutputStream xmlOut = openOutputStream(relativePath);
-//                     try {
-//                             session.exportSystemView("/", xmlOut, false, false);
-//                     } finally {
-//                             closeOutputStream(relativePath, xmlOut);
-//                     }
-
-                       // TODO scan all binaries
+                       if (log.isDebugEnabled())
+                               log.debug(workspaceName + ":" + contentPaths.size() + " files exported to " + workspacesFilesBasePath);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot backup files from " + workspaceName + ":", e);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot backup files from " + workspaceName + ":", e);
                } finally {
-                       JcrUtils.logoutQuietly(session);
+                       Jcr.logout(session);
                }
        }
 
@@ -286,4 +273,66 @@ public class LogicalBackup implements Runnable {
                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";
+                       Dictionary<String, String> headers = bundle.getHeaders();
+                       Manifest manifest = new Manifest();
+                       Enumeration<String> headerKeys = headers.keys();
+                       while (headerKeys.hasMoreElements()) {
+                               String headerKey = headerKeys.nextElement();
+                               String headerValue = headers.get(headerKey);
+                               manifest.getMainAttributes().putValue(headerKey, headerValue);
+                       }
+                       try (JarOutputStream jarOut = new JarOutputStream(openOutputStream(relativePath), manifest)) {
+                               Enumeration<URL> resourcePaths = bundle.findEntries("/", "*", true);
+                               resources: while (resourcePaths.hasMoreElements()) {
+                                       URL entryUrl = resourcePaths.nextElement();
+                                       String entryPath = entryUrl.getPath();
+                                       if (entryPath.equals(""))
+                                               continue resources;
+                                       if (entryPath.endsWith("/"))
+                                               continue resources;
+                                       String entryName = entryPath.substring(1);// remove first '/'
+                                       if (entryUrl.getPath().equals("/META-INF/"))
+                                               continue resources;
+                                       if (entryUrl.getPath().equals("/META-INF/MANIFEST.MF"))
+                                               continue resources;
+                                       // dev
+                                       if (entryUrl.getPath().startsWith("/target"))
+                                               continue resources;
+                                       if (entryUrl.getPath().startsWith("/src"))
+                                               continue resources;
+                                       if (entryUrl.getPath().startsWith("/ext"))
+                                               continue resources;
+
+                                       if (entryName.startsWith("bin/")) {// dev
+                                               entryName = entryName.substring("bin/".length());
+                                       }
+
+                                       ZipEntry entry = new ZipEntry(entryName);
+                                       try (InputStream in = entryUrl.openStream()) {
+                                               try {
+                                                       jarOut.putNextEntry(entry);
+                                               } catch (ZipException e) {// duplicate
+                                                       continue resources;
+                                               }
+                                               IOUtils.copy(in, jarOut);
+                                               jarOut.closeEntry();
+//                                             log.info(entryUrl);
+                                       } catch (FileNotFoundException e) {
+                                               log.warn(entryUrl + ": " + e.getMessage());
+                                       }
+                               }
+                       } catch (IOException e1) {
+                               throw new RuntimeException("Cannot export bundle " + bundle, e1);
+                       }
+               }
+               if (log.isDebugEnabled())
+                       log.debug(bundles.length + " OSGi bundles exported to " + bootBasePath);
+
+       }
+
 }
index b430af6e1190d82818a6f2a0f6c0e0b8bca40f69..145c5bb3dac374f0bf974bba99b2682dd5d0e744 100644 (file)
@@ -11,13 +11,17 @@ import javax.jcr.Repository;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 
-import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.jcr.Jcr;
 import org.argeo.jcr.JcrException;
 import org.argeo.jcr.JcrUtils;
 import org.osgi.framework.BundleContext;
 
 /** Restores a backup in the format defined by {@link LogicalBackup}. */
 public class LogicalRestore implements Runnable {
+       private final static Log log = LogFactory.getLog(LogicalRestore.class);
+
        private final Repository repository;
        private final BundleContext bundleContext;
        private final Path basePath;
@@ -31,15 +35,48 @@ public class LogicalRestore implements Runnable {
        @Override
        public void run() {
                Path workspaces = basePath.resolve(LogicalBackup.WORKSPACES_BASE);
-               try (DirectoryStream<Path> xmls = Files.newDirectoryStream(workspaces, "*.xml")) {
-                       for (Path workspacePath : xmls) {
-                               String workspaceName = FilenameUtils.getBaseName(workspacePath.getFileName().toString());
-                               Session session = JcrUtils.loginOrCreateWorkspace(repository, workspaceName);
-                               try (InputStream in = Files.newInputStream(workspacePath)) {
-                                       session.getWorkspace().importXML("/", in,
-                                                       ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
-                               } finally {
-                                       JcrUtils.logoutQuietly(session);
+               try {
+                       // import jcr:system first
+                       try (DirectoryStream<Path> workspaceDirs = Files.newDirectoryStream(workspaces)) {
+                               dirs: for (Path workspacePath : workspaceDirs) {
+                                       String workspaceName = workspacePath.getFileName().toString();
+                                       try (DirectoryStream<Path> xmls = Files.newDirectoryStream(workspacePath, "*.xml")) {
+                                               for (Path xml : xmls) {
+                                                       if (xml.getFileName().toString().equals("jcr:system.xml")) {
+                                                               Session session = JcrUtils.loginOrCreateWorkspace(repository, workspaceName);
+                                                               try (InputStream in = Files.newInputStream(xml)) {
+                                                                       session.getWorkspace().importXML("/", in,
+                                                                                       ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
+                                                                       if (log.isDebugEnabled())
+                                                                               log.debug("Restored " + xml + " to workspace " + workspaceName);
+                                                                       break dirs;
+                                                               } finally {
+                                                                       Jcr.logout(session);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       // non-system content
+                       try (DirectoryStream<Path> workspaceDirs = Files.newDirectoryStream(workspaces)) {
+                               for (Path workspacePath : workspaceDirs) {
+                                       String workspaceName = workspacePath.getFileName().toString();
+                                       Session session = JcrUtils.loginOrCreateWorkspace(repository, workspaceName);
+                                       try (DirectoryStream<Path> xmls = Files.newDirectoryStream(workspacePath, "*.xml")) {
+                                               xmls: for (Path xml : xmls) {
+                                                       if (xml.getFileName().toString().equals("jcr:system.xml"))
+                                                               continue xmls;
+                                                       try (InputStream in = Files.newInputStream(xml)) {
+                                                               session.getWorkspace().importXML("/", in,
+                                                                               ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
+                                                               if (log.isDebugEnabled())
+                                                                       log.debug("Restored " + xml + " to workspace " + workspaceName);
+                                                       }
+                                               }
+                                       } finally {
+                                               Jcr.logout(session);
+                                       }
                                }
                        }
                } catch (IOException e) {