diff --git a/vault/src/main/java/org/opencadc/vault/JobAbortedException.java b/vault/src/main/java/org/opencadc/vault/JobAbortedException.java
new file mode 100644
index 00000000..64f78bcb
--- /dev/null
+++ b/vault/src/main/java/org/opencadc/vault/JobAbortedException.java
@@ -0,0 +1,105 @@
+/*
+************************************************************************
+******************* CANADIAN ASTRONOMY DATA CENTRE *******************
+************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES **************
+*
+* (c) 2023. (c) 2023.
+* Government of Canada Gouvernement du Canada
+* National Research Council Conseil national de recherches
+* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6
+* All rights reserved Tous droits réservés
+*
+* NRC disclaims any warranties, Le CNRC dénie toute garantie
+* expressed, implied, or énoncée, implicite ou légale,
+* statutory, of any kind with de quelque nature que ce
+* respect to the software, soit, concernant le logiciel,
+* including without limitation y compris sans restriction
+* any warranty of merchantability toute garantie de valeur
+* or fitness for a particular marchande ou de pertinence
+* purpose. NRC shall not be pour un usage particulier.
+* liable in any event for any Le CNRC ne pourra en aucun cas
+* damages, whether direct or être tenu responsable de tout
+* indirect, special or general, dommage, direct ou indirect,
+* consequential or incidental, particulier ou général,
+* arising from the use of the accessoire ou fortuit, résultant
+* software. Neither the name de l'utilisation du logiciel. Ni
+* of the National Research le nom du Conseil National de
+* Council of Canada nor the Recherches du Canada ni les noms
+* names of its contributors may de ses participants ne peuvent
+* be used to endorse or promote être utilisés pour approuver ou
+* products derived from this promouvoir les produits dérivés
+* software without specific prior de ce logiciel sans autorisation
+* written permission. préalable et particulière
+* par écrit.
+*
+* This file is part of the Ce fichier fait partie du projet
+* OpenCADC project. OpenCADC.
+*
+* OpenCADC is free software: OpenCADC est un logiciel libre ;
+* you can redistribute it and/or vous pouvez le redistribuer ou le
+* modify it under the terms of modifier suivant les termes de
+* the GNU Affero General Public la “GNU Affero General Public
+* License as published by the License” telle que publiée
+* Free Software Foundation, par la Free Software Foundation
+* either version 3 of the : soit la version 3 de cette
+* License, or (at your option) licence, soit (à votre gré)
+* any later version. toute version ultérieure.
+*
+* OpenCADC is distributed in the OpenCADC est distribué
+* hope that it will be useful, dans l’espoir qu’il vous
+* but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE
+* without even the implied GARANTIE : sans même la garantie
+* warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ
+* or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF
+* PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence
+* General Public License for Générale Publique GNU Affero
+* more details. pour plus de détails.
+*
+* You should have received Vous devriez avoir reçu une
+* a copy of the GNU Affero copie de la Licence Générale
+* General Public License along Publique GNU Affero avec
+* with OpenCADC. If not, see OpenCADC ; si ce n’est
+* . pas le cas, consultez :
+* .
+*
+* $Revision: 4 $
+*
+************************************************************************
+*/
+
+package org.opencadc.vault;
+
+import ca.nrc.cadc.uws.Job;
+import org.opencadc.vospace.VOSException;
+
+/**
+ * Exception indicating the a job has been aborted by the client.
+ *
+ * @author yeunga
+ *
+ */
+public class JobAbortedException extends VOSException
+{
+
+ private static final long serialVersionUID = -4453433718383619175L;
+
+ private Job job;
+
+ public JobAbortedException(Job job)
+ {
+ super("job aborted");
+ this.job = job;
+ }
+
+ public JobAbortedException(Job job, String message)
+ {
+ super(message);
+ this.job = job;
+ }
+
+ public Job getJob()
+ {
+ return job;
+ }
+
+}
diff --git a/vault/src/main/java/org/opencadc/vault/JobManager.java b/vault/src/main/java/org/opencadc/vault/JobManager.java
new file mode 100644
index 00000000..44df0c43
--- /dev/null
+++ b/vault/src/main/java/org/opencadc/vault/JobManager.java
@@ -0,0 +1,88 @@
+/*
+************************************************************************
+******************* CANADIAN ASTRONOMY DATA CENTRE *******************
+************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES **************
+*
+* (c) 2023. (c) 2023.
+* Government of Canada Gouvernement du Canada
+* National Research Council Conseil national de recherches
+* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6
+* All rights reserved Tous droits réservés
+*
+* NRC disclaims any warranties, Le CNRC dénie toute garantie
+* expressed, implied, or énoncée, implicite ou légale,
+* statutory, of any kind with de quelque nature que ce
+* respect to the software, soit, concernant le logiciel,
+* including without limitation y compris sans restriction
+* any warranty of merchantability toute garantie de valeur
+* or fitness for a particular marchande ou de pertinence
+* purpose. NRC shall not be pour un usage particulier.
+* liable in any event for any Le CNRC ne pourra en aucun cas
+* damages, whether direct or être tenu responsable de tout
+* indirect, special or general, dommage, direct ou indirect,
+* consequential or incidental, particulier ou général,
+* arising from the use of the accessoire ou fortuit, résultant
+* software. Neither the name de l'utilisation du logiciel. Ni
+* of the National Research le nom du Conseil National de
+* Council of Canada nor the Recherches du Canada ni les noms
+* names of its contributors may de ses participants ne peuvent
+* be used to endorse or promote être utilisés pour approuver ou
+* products derived from this promouvoir les produits dérivés
+* software without specific prior de ce logiciel sans autorisation
+* written permission. préalable et particulière
+* par écrit.
+*
+* This file is part of the Ce fichier fait partie du projet
+* OpenCADC project. OpenCADC.
+*
+* OpenCADC is free software: OpenCADC est un logiciel libre ;
+* you can redistribute it and/or vous pouvez le redistribuer ou le
+* modify it under the terms of modifier suivant les termes de
+* the GNU Affero General Public la “GNU Affero General Public
+* License as published by the License” telle que publiée
+* Free Software Foundation, par la Free Software Foundation
+* either version 3 of the : soit la version 3 de cette
+* License, or (at your option) licence, soit (à votre gré)
+* any later version. toute version ultérieure.
+*
+* OpenCADC is distributed in the OpenCADC est distribué
+* hope that it will be useful, dans l’espoir qu’il vous
+* but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE
+* without even the implied GARANTIE : sans même la garantie
+* warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ
+* or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF
+* PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence
+* General Public License for Générale Publique GNU Affero
+* more details. pour plus de détails.
+*
+* You should have received Vous devriez avoir reçu une
+* a copy of the GNU Affero copie de la Licence Générale
+* General Public License along Publique GNU Affero avec
+* with OpenCADC. If not, see OpenCADC ; si ce n’est
+* . pas le cas, consultez :
+* .
+*
+************************************************************************
+*/
+
+package org.opencadc.vault;
+
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.uws.server.JobPersistence;
+import ca.nrc.cadc.uws.server.SimpleJobManager;
+import ca.nrc.cadc.uws.server.impl.PostgresJobPersistence;
+import org.apache.log4j.Logger;
+
+public abstract class JobManager extends SimpleJobManager
+{
+
+ private static final Logger log = Logger.getLogger(JobManager.class);
+
+ protected static JobPersistence jp;
+
+ static {
+ log.info("Creating shared (postgres) job manager");
+ jp = new PostgresJobPersistence(AuthenticationUtil.getIdentityManager());
+ }
+
+}
diff --git a/vault/src/main/java/org/opencadc/vault/RecursiveDeleteNodeJobManager.java b/vault/src/main/java/org/opencadc/vault/RecursiveDeleteNodeJobManager.java
new file mode 100644
index 00000000..f857dfc8
--- /dev/null
+++ b/vault/src/main/java/org/opencadc/vault/RecursiveDeleteNodeJobManager.java
@@ -0,0 +1,103 @@
+/*
+************************************************************************
+******************* CANADIAN ASTRONOMY DATA CENTRE *******************
+************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES **************
+*
+* (c) 2023. (c) 2023.
+* Government of Canada Gouvernement du Canada
+* National Research Council Conseil national de recherches
+* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6
+* All rights reserved Tous droits réservés
+*
+* NRC disclaims any warranties, Le CNRC dénie toute garantie
+* expressed, implied, or énoncée, implicite ou légale,
+* statutory, of any kind with de quelque nature que ce
+* respect to the software, soit, concernant le logiciel,
+* including without limitation y compris sans restriction
+* any warranty of merchantability toute garantie de valeur
+* or fitness for a particular marchande ou de pertinence
+* purpose. NRC shall not be pour un usage particulier.
+* liable in any event for any Le CNRC ne pourra en aucun cas
+* damages, whether direct or être tenu responsable de tout
+* indirect, special or general, dommage, direct ou indirect,
+* consequential or incidental, particulier ou général,
+* arising from the use of the accessoire ou fortuit, résultant
+* software. Neither the name de l'utilisation du logiciel. Ni
+* of the National Research le nom du Conseil National de
+* Council of Canada nor the Recherches du Canada ni les noms
+* names of its contributors may de ses participants ne peuvent
+* be used to endorse or promote être utilisés pour approuver ou
+* products derived from this promouvoir les produits dérivés
+* software without specific prior de ce logiciel sans autorisation
+* written permission. préalable et particulière
+* par écrit.
+*
+* This file is part of the Ce fichier fait partie du projet
+* OpenCADC project. OpenCADC.
+*
+* OpenCADC is free software: OpenCADC est un logiciel libre ;
+* you can redistribute it and/or vous pouvez le redistribuer ou le
+* modify it under the terms of modifier suivant les termes de
+* the GNU Affero General Public la “GNU Affero General Public
+* License as published by the License” telle que publiée
+* Free Software Foundation, par la Free Software Foundation
+* either version 3 of the : soit la version 3 de cette
+* License, or (at your option) licence, soit (à votre gré)
+* any later version. toute version ultérieure.
+*
+* OpenCADC is distributed in the OpenCADC est distribué
+* hope that it will be useful, dans l’espoir qu’il vous
+* but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE
+* without even the implied GARANTIE : sans même la garantie
+* warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ
+* or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF
+* PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence
+* General Public License for Générale Publique GNU Affero
+* more details. pour plus de détails.
+*
+* You should have received Vous devriez avoir reçu une
+* a copy of the GNU Affero copie de la Licence Générale
+* General Public License along Publique GNU Affero avec
+* with OpenCADC. If not, see OpenCADC ; si ce n’est
+* . pas le cas, consultez :
+* .
+*
+* $Revision: 4 $
+*
+************************************************************************
+*/
+
+package org.opencadc.vault;
+
+import ca.nrc.cadc.uws.server.JobExecutor;
+import ca.nrc.cadc.uws.server.JobUpdater;
+import ca.nrc.cadc.uws.server.ThreadPoolExecutor;
+import org.apache.log4j.Logger;
+
+/**
+ *
+ * @author pdowler, majorb, yeunga, adriand
+ */
+public class RecursiveDeleteNodeJobManager extends JobManager
+{
+ private static final Logger log = Logger.getLogger(RecursiveDeleteNodeJobManager.class);
+
+ private static final Long MAX_EXEC_DURATION = Long.valueOf(12*7200L); // 24 hours?
+ private static final Long MAX_DESTRUCTION = Long.valueOf(7*24*3600L); // 1 week
+ private static final Long MAX_QUOTE = Long.valueOf(12*7200L); // same as exec
+
+ public RecursiveDeleteNodeJobManager()
+ {
+ super();
+ // jp is instantiated in parent org.opencadc.cavern.JobManager
+ JobUpdater ju = jp;
+ super.setJobPersistence(jp);
+
+ JobExecutor jobExec = new ThreadPoolExecutor(ju, RecursiveDeleteNodeRunner.class, 3);
+ super.setJobExecutor(jobExec);
+
+ super.setMaxExecDuration(MAX_EXEC_DURATION);
+ super.setMaxDestruction(MAX_DESTRUCTION);
+ super.setMaxQuote(MAX_QUOTE);
+ }
+}
diff --git a/vault/src/main/java/org/opencadc/vault/RecursiveDeleteNodeRunner.java b/vault/src/main/java/org/opencadc/vault/RecursiveDeleteNodeRunner.java
new file mode 100644
index 00000000..ac9a0b65
--- /dev/null
+++ b/vault/src/main/java/org/opencadc/vault/RecursiveDeleteNodeRunner.java
@@ -0,0 +1,350 @@
+/*
+************************************************************************
+******************* CANADIAN ASTRONOMY DATA CENTRE *******************
+************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES **************
+*
+* (c) 2023. (c) 2023.
+* Government of Canada Gouvernement du Canada
+* National Research Council Conseil national de recherches
+* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6
+* All rights reserved Tous droits réservés
+*
+* NRC disclaims any warranties, Le CNRC dénie toute garantie
+* expressed, implied, or énoncée, implicite ou légale,
+* statutory, of any kind with de quelque nature que ce
+* respect to the software, soit, concernant le logiciel,
+* including without limitation y compris sans restriction
+* any warranty of merchantability toute garantie de valeur
+* or fitness for a particular marchande ou de pertinence
+* purpose. NRC shall not be pour un usage particulier.
+* liable in any event for any Le CNRC ne pourra en aucun cas
+* damages, whether direct or être tenu responsable de tout
+* indirect, special or general, dommage, direct ou indirect,
+* consequential or incidental, particulier ou général,
+* arising from the use of the accessoire ou fortuit, résultant
+* software. Neither the name de l'utilisation du logiciel. Ni
+* of the National Research le nom du Conseil National de
+* Council of Canada nor the Recherches du Canada ni les noms
+* names of its contributors may de ses participants ne peuvent
+* be used to endorse or promote être utilisés pour approuver ou
+* products derived from this promouvoir les produits dérivés
+* software without specific prior de ce logiciel sans autorisation
+* written permission. préalable et particulière
+* par écrit.
+*
+* $Revision: 1 $
+*
+************************************************************************
+*/
+
+package org.opencadc.vault;
+
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.io.ResourceIterator;
+import ca.nrc.cadc.rest.SyncOutput;
+import ca.nrc.cadc.util.MultiValuedProperties;
+import ca.nrc.cadc.util.ThrowableUtil;
+import ca.nrc.cadc.uws.ErrorSummary;
+import ca.nrc.cadc.uws.ErrorType;
+import ca.nrc.cadc.uws.ExecutionPhase;
+import ca.nrc.cadc.uws.Job;
+import ca.nrc.cadc.uws.Parameter;
+import ca.nrc.cadc.uws.Result;
+import ca.nrc.cadc.uws.server.JobRunner;
+import ca.nrc.cadc.uws.server.JobUpdater;
+import ca.nrc.cadc.uws.util.JobLogInfo;
+import java.io.FileNotFoundException;
+import java.net.URI;
+import java.security.AccessControlException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import javax.security.auth.Subject;
+import org.apache.log4j.Logger;
+import org.opencadc.vospace.ContainerNode;
+import org.opencadc.vospace.Node;
+import org.opencadc.vospace.VOSURI;
+import org.opencadc.vospace.server.NodeFault;
+import org.opencadc.vospace.server.NodePersistence;
+import org.opencadc.vospace.server.PathResolver;
+import org.opencadc.vospace.server.Utils;
+import org.opencadc.vospace.server.auth.VOSpaceAuthorizer;
+
+/**
+ * Class to delete the target node and all child nodes recursively.
+ *
+ *
+ */
+public class RecursiveDeleteNodeRunner implements JobRunner {
+
+ private static Logger log = Logger.getLogger(RecursiveDeleteNodeRunner.class);
+
+ // containers are not locked while being emptied for the recursive delete and might need a few revisits in order
+ // for the operation to succeed.
+ private static final long PHASE_CHECK_INTERVAL = 1000; // 1 second
+ private static final long MAX_ERROR_BEFORE_ABORT = 100;
+
+ private Job job;
+ private JobUpdater jobUpdater;
+ private long lastPhaseCheck = System.currentTimeMillis();
+ private VOSpaceAuthorizer vospaceAuthorizer;
+ private NodePersistence nodePersistence;
+ private long deleteCount = 0;
+ private long errorCount = 0;
+ private JobLogInfo logInfo;
+
+ public URI resourceID;
+
+ public RecursiveDeleteNodeRunner() {
+ MultiValuedProperties mvp = VaultInitAction.getConfig();
+ String resourceID = mvp.getFirstPropertyValue(VaultInitAction.RESOURCE_ID_KEY);
+ this.resourceID = URI.create(resourceID);
+ }
+
+ @Override
+ public void setJobUpdater(JobUpdater jobUpdater) {
+ this.jobUpdater = jobUpdater;
+ }
+
+ @Override
+ public void setJob(Job job) {
+ this.job = job;
+ }
+
+ @Override
+ public void setSyncOutput(SyncOutput syncOutput) {
+ // not used
+ }
+
+ @Override
+ public void run() {
+ log.debug("RUN RecursiveDeleteNodeRunner");
+ logInfo = new JobLogInfo(job);
+
+ String startMessage = logInfo.start();
+ log.info(startMessage);
+
+ long t1 = System.currentTimeMillis();
+ doit();
+ long t2 = System.currentTimeMillis();
+
+ logInfo.setElapsedTime(t2 - t1);
+
+ String endMessage = logInfo.end();
+ log.info(endMessage);
+ }
+
+ private void doit() {
+ try {
+ // set the phase to executing
+ ExecutionPhase ep = jobUpdater.setPhase(
+ job.getID(), ExecutionPhase.QUEUED, ExecutionPhase.EXECUTING, new Date());
+
+ if (ep == null) {
+ throw new IllegalStateException(
+ "Could not change the job phase from " + ExecutionPhase.QUEUED +
+ " to " + ExecutionPhase.EXECUTING);
+ }
+
+ VOSURI nodeURI = null;
+ for (Parameter param : job.getParameterList()) {
+ if (param.getName().equalsIgnoreCase("nodeURI")) {
+ nodeURI = new VOSURI(URI.create(param.getValue()));
+ break;
+ }
+ }
+
+ if (nodeURI == null) {
+ throw new IllegalArgumentException("nodeURI argument required");
+ }
+
+ log.debug("node: " + nodeURI);
+
+ // Create the node persistence and authorizer objects
+ //TODO move to the library
+ this.nodePersistence = new NodePersistenceImpl(resourceID);
+ vospaceAuthorizer = new VOSpaceAuthorizer(nodePersistence);
+
+ PathResolver pathResolver = new PathResolver(nodePersistence, vospaceAuthorizer, true);
+ String nodePath = nodeURI.getPath();
+ Node serverNode = pathResolver.getNode(nodePath);
+ if (serverNode == null) {
+ throw NodeFault.NodeNotFound.getStatus(nodePath);
+ }
+
+ ContainerNode parent = serverNode.parent;
+ Subject caller = AuthenticationUtil.getCurrentSubject();
+ if (!vospaceAuthorizer.hasSingleNodeWritePermission(parent, caller)) {
+ throw NodeFault.PermissionDenied.getStatus(nodePath);
+ }
+
+ if (serverNode instanceof ContainerNode) {
+ try {
+ deleteContainer((ContainerNode) serverNode, caller);
+ } catch (JobAbortedException ex) {
+ // nothing to do here
+ }
+ } else {
+ try {
+ nodePersistence.delete(serverNode);
+ log.debug("Recursively deleted data node " + nodePath);
+ } catch (Exception ex) {
+ log.debug("Cannot recursively delete node " + nodePath, ex);
+ incErrorCount();
+ }
+ }
+
+ // set the appropriate end phase and error summary
+ ErrorSummary error = null;
+ ExecutionPhase endPhase = ExecutionPhase.COMPLETED;
+ if (deleteCount == 0) {
+ endPhase = ExecutionPhase.ERROR;
+ ep = jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING, endPhase, error, new Date());
+ } else {
+ List results = new ArrayList<>();
+ results.add(new Result("delcount", URI.create("final:" + deleteCount)));
+ if (errorCount > 0) {
+ endPhase = ExecutionPhase.ABORTED;
+ results.add(new Result("errorcount", URI.create("final:" + errorCount)));
+ ep = jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING, endPhase, results, new Date());
+ } else {
+ ep = jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING, endPhase, results, new Date());
+ }
+ }
+ if (!endPhase.equals(ep)) {
+ log.warn("Could not change the job phase from " + ExecutionPhase.EXECUTING +
+ " to " + endPhase);
+ }
+ } catch (FileNotFoundException e) {
+ sendError("NotFound");
+ } catch (AccessControlException e) {
+ sendError("PermissionDenied");
+ } catch (Throwable t) {
+ log.error("Unexpected exception", t);
+ // if the cause of this throwable is an InterruptedException, and if
+ // the job has been aborted, then this can be considered a normal
+ // abort procedure
+ if (t instanceof InterruptedException || ThrowableUtil.isACause(t, InterruptedException.class)) {
+ try {
+ ExecutionPhase ep = jobUpdater.getPhase(job.getID());
+ if (ExecutionPhase.ABORTED.equals(ep)) {
+ return;
+ }
+ } catch (Exception e) {
+ log.error("Could not check job phase: ", e);
+ }
+ }
+
+ sendError("Unexpected Exception: " + t.getMessage());
+ }
+ }
+
+ private boolean deleteContainer(ContainerNode node, Subject caller) throws Exception {
+
+ boolean errors = false;
+ List childContainers = new ArrayList<>();
+ log.debug("Deleting container " + Utils.getPath(node));
+ ResourceIterator iterator = nodePersistence.iterator(node, null, null);
+ while (iterator.hasNext()) {
+ checkJobPhase();
+ Node child = iterator.next();
+ if (child instanceof ContainerNode) {
+ childContainers.add((ContainerNode) child);
+ } else {
+ try {
+ if (!vospaceAuthorizer.hasSingleNodeWritePermission(child.parent, caller)) {
+ throw NodeFault.PermissionDenied.getStatus(Utils.getPath(child));
+ }
+ nodePersistence.delete(child);
+ log.debug("Recursive deleted non-container node " + Utils.getPath(child));
+ deleteCount++;
+ } catch (Exception ex) {
+ log.debug("Failed to (recursive) delete non-container node " + Utils.getPath(child), ex);
+ errors = true;
+ incErrorCount();
+ }
+ }
+ }
+ if (!childContainers.isEmpty()) {
+ // descend down one level
+ for (ContainerNode cn : childContainers) {
+ errors |= deleteContainer(cn, caller);
+ }
+ }
+ if (!errors) {
+ // delete the empty container
+ try {
+ if (!vospaceAuthorizer.hasSingleNodeWritePermission(node.parent, caller)) {
+ throw NodeFault.PermissionDenied.getStatus(Utils.getPath(node));
+ }
+ nodePersistence.delete(node);
+ log.debug("Recursive deleted container node " + Utils.getPath(node));
+ deleteCount++;
+ } catch (Exception e) {
+ log.debug("Failed to (recursive) delete container node " + Utils.getPath(node), e);
+ incErrorCount();
+ errors = true;
+ }
+ }
+ return errors;
+ }
+
+ private void incErrorCount() throws JobAbortedException {
+ if (++errorCount > MAX_ERROR_BEFORE_ABORT) {
+ throw new JobAbortedException(job);
+ }
+ }
+
+ /**
+ * If a minimum of PHASE_CHECK_INTERVAL time has passed, check
+ * to ensure the job is still in the EXECUTING PHASE
+ */
+ private void checkJobPhase()
+ throws Exception {
+ // only check phase if a minimum of PHASE_CHECK_INTERVAL time has passed
+ long now = System.currentTimeMillis();
+ long diff = now - lastPhaseCheck;
+ log.debug("Last phase check diff: " + diff);
+ if (diff > PHASE_CHECK_INTERVAL) {
+ // reset the last phase check time
+ lastPhaseCheck = now;
+
+ log.debug("Checking job phase");
+ ExecutionPhase ep = jobUpdater.getPhase(job.getID());
+ log.debug("Job phase is: " + ep);
+ if (ExecutionPhase.ABORTED.equals(ep)) {
+ throw new JobAbortedException(job);
+ }
+
+ if (!ExecutionPhase.EXECUTING.equals(ep)) {
+ throw new IllegalStateException("Job should be in phase " +
+ ExecutionPhase.EXECUTING + " but is in phase " + ep);
+ }
+ }
+ }
+
+ /**
+ * Set the job execution phase to error and save the error message.
+ * @param message - error message
+ */
+ private void sendError(String message) {
+ logInfo.setSuccess(false);
+ logInfo.setMessage(message);
+
+ // set the phase to error
+ ErrorSummary error = new ErrorSummary(message, ErrorType.FATAL);
+
+ try {
+ ExecutionPhase ep = jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING,
+ ExecutionPhase.ERROR, error, new Date());
+
+ if (!ExecutionPhase.ERROR.equals(ep)) {
+ log.warn("Could not change the job phase from " + ExecutionPhase.EXECUTING +
+ " to " + ExecutionPhase.ERROR + " because it is " + jobUpdater.getPhase(job.getID()));
+ }
+ } catch (Throwable t) {
+ log.error("Failed to change the job phase from " + ExecutionPhase.EXECUTING +
+ " to " + ExecutionPhase.ERROR, t);
+ }
+ }
+}
diff --git a/vault/src/main/resources/VOSpacePlugins.properties b/vault/src/main/resources/VOSpacePlugins.properties
new file mode 100644
index 00000000..7d3e0042
--- /dev/null
+++ b/vault/src/main/resources/VOSpacePlugins.properties
@@ -0,0 +1,12 @@
+#
+# Configuration information for the vospace storage interface
+#
+
+# Define the class that implements the interface TransferGenerator
+# This class will be loaded at runtime to handle transfer requests.
+# ca.nrc.cadc.vos.transfers.TransferGenerator =
+ca.nrc.cadc.vos.server.transfers.TransferGenerator = ca.nrc.cadc.vospace.transfers.ADTransferGenerator
+
+# Define an optional class that implements the interface NodePersistence
+# This class will be loaded at runtime to node persistence operations.
+ca.nrc.cadc.vos.server.NodePersistence = ca.nrc.cadc.vospace.VOSpaceNodePersistence
\ No newline at end of file
diff --git a/vault/src/main/resources/Views.properties b/vault/src/main/resources/Views.properties
new file mode 100644
index 00000000..cde5e1e1
--- /dev/null
+++ b/vault/src/main/resources/Views.properties
@@ -0,0 +1,72 @@
+###############################################################################
+#
+# Views.properties
+#
+# Defines the views available in this instance of VOSpace. These are loaded
+# upon the loading of class ca.nrc.cadc.vos.ViewFactory into the VM.
+#
+# The names of the views to be defined must be listed under the 'views' key
+# and separated by a space. For example:
+# views = ...
+#
+# For each view name listed, three settings must be defined
+# uri = The URI of the view
+# alias = The alias (or shortcut) name of the view
+# class = The implementing class of the view
+#
+# Additionally, two optional settings may be defined:
+# accepts = true/false, true if this services accepts the view
+# provides = true/false, true if this service provides the view
+#
+# For example:
+# .uri = ivo://cadc.nrc.ca/vospace/core#dataview
+# .alias = data
+# .class = ca.nrc.cadc.vos.DataView
+# .accepts = false
+# .provides = true
+#
+# Notes:
+# - View classes must extend class ca.nrc.cadc.vos.AbstractView
+# - There cannot be a duplicate of any aliases or URIs in any of the
+# view definitions
+#
+###############################################################################
+
+#views = data rss manifest cutout header
+views = data
+
+# data view definition
+data.uri = ivo://cadc.nrc.ca/vospace/view#data
+data.alias = data
+data.class = org.opencadc.vospace.server.DataView
+data.accepts = false
+data.provides = true
+
+# rss view definition
+rss.uri = ivo://cadc.nrc.ca/vospace/view#rss
+rss.alias = rss
+rss.class = ca.nrc.cadc.vos.server.RssView
+rss.accepts = false
+rss.provides = true
+
+# manifest view definition
+manifest.uri = ivo://cadc.nrc.ca/vospace/view#manifest
+manifest.alias = manifest
+manifest.class = ca.nrc.cadc.vos.server.ManifestView
+manifest.accepts = false
+manifest.provides = true
+
+# cutout view definition
+cutout.uri = ivo://cadc.nrc.ca/vospace/view#cutout
+cutout.alias = cutout
+cutout.class = ca.nrc.cadc.vospace.CutoutView
+cutout.accepts = false
+cutout.provides = true
+
+# header view definition
+header.uri = ivo://cadc.nrc.ca/vospace/view#header
+header.alias = header
+header.class = ca.nrc.cadc.vospace.HeaderView
+header.accepts = false
+header.provides = true
+
diff --git a/vault/src/main/webapp/service.yaml b/vault/src/main/webapp/service.yaml
new file mode 100644
index 00000000..27bcda43
--- /dev/null
+++ b/vault/src/main/webapp/service.yaml
@@ -0,0 +1,335 @@
+swagger: '2.0'
+info:
+ version: 2.1.0
+ title: VOSpace
+ description: |
+ The CANFAR Vault web service, a VOSpace storage implementation.
+
+ VOSpace is the IVOA (International Virtual Observatory Aliance) standard interface to distributed storage. This VOSpace web service is an access point for a distributed storage network. There are three main functions of the VOSpace service:
+ 1. Add, replace, or delete data objects in a tree data structure.
+ 2. Manipulate the metadata for the data objects.
+ 3. Obtain URIs through which the content of the data objects can be accessed.
+
+ Please see the specification for full details: VOSpace 2.1 Specification
+
+ Interaction with the synchronous and asynchronous data object transfer endpoints use the job management patterns defined in the IVOA Universal Worker Service (UWS) specification: UWS 1.1 Specification
+schemes:
+ - https
+basePath: /vault
+paths:
+ /nodes/{nodePath}:
+ put:
+ description: |
+ Create a new node at a specified location
+ tags:
+ - Nodes
+ consumes:
+ - text/xml
+ responses:
+ '200':
+ description: Successful response
+ '201':
+ description: Successful response
+ '403':
+ description: If the user does not have permission.
+ '404':
+ description: If the path to the node could not be found.
+ '409':
+ description: If the node already exits.
+ '500':
+ description: Internal error
+ '503':
+ description: Service busy
+ default:
+ description: Unexpeced error
+ schema:
+ $ref: '#/definitions/Error'
+ parameters:
+ - name: Node
+ in: body
+ description: The new Node
+ required: true
+ schema:
+ $ref: '#/definitions/Node'
+ delete:
+ description: |
+ Delete a node. When the target is a ContainerNode, all its children (the contents of the container) SHALL also be deleted.
+ tags:
+ - Nodes
+ responses:
+ '200':
+ description: Successful response
+ '204':
+ description: Successful response
+ '403':
+ description: If the user does not have permission.
+ '404':
+ description: If the path to the node could not be found.
+ '500':
+ description: Internal error
+ '503':
+ description: Service busy
+ default:
+ description: Unexpeced error
+ schema:
+ $ref: '#/definitions/Error'
+ get:
+ description: |
+ Get the details for a specific Node.
+ tags:
+ - Nodes
+ responses:
+ '200':
+ description: Successful response
+ schema:
+ $ref: '#/definitions/Node'
+ '403':
+ description: If the user does not have permission.
+ '404':
+ description: If the path to the node could not be found.
+ '500':
+ description: Internal error
+ '503':
+ description: Service busy
+ default:
+ description: Unexpeced error
+ schema:
+ $ref: '#/definitions/Error'
+ parameters:
+ - name: nodePath
+ in: path
+ description: The path for the node
+ required: true
+ type: string
+ post:
+ description: |
+ Set the property values for a specific Node
+ tags:
+ - Nodes
+ consumes:
+ - text/xml
+ responses:
+ '200':
+ description: Successful response
+ '201':
+ description: Successful response
+ '403':
+ description: If the user does not have permission.
+ '404':
+ description: If the path to the node could not be found.
+ '500':
+ description: Internal error
+ '503':
+ description: Service busy
+ default:
+ description: Unexpeced error
+ schema:
+ $ref: '#/definitions/Error'
+ parameters:
+ - name: Node
+ in: body
+ description: The updated Node
+ required: true
+ schema:
+ $ref: '#/definitions/Node'
+ parameters:
+ - name: nodePath
+ in: path
+ description: The path for the node
+ required: true
+ type: string
+ /recursiveDelete:
+ post:
+ description: |
+ Post a recursive delete command. This is an IVOA UWS end point.
+ tags:
+ - Recursive delete
+ consumes:
+ - None
+ responses:
+ '200':
+ description: Successful response
+ '201':
+ description: Successful response
+ '403':
+ description: If the user does not have permission.
+ '404':
+ description: If the source node could not be found.
+ '409':
+ description: If the destination node already exists.
+ '500':
+ description: Internal error
+ '503':
+ description: Service busy
+ default:
+ description: Unexpeced error
+ schema:
+ $ref: '#/definitions/Error'
+ parameters:
+ - name: nodeURI
+ in: query
+ description: The base node (typically a container) to recursively delete
+ required: true
+ type: string
+ format: uri
+ /availability:
+ get:
+ tags:
+ - Support Interfaces
+ summary: VOSI Availability
+ description: Indicates whether the service is operable and shows the reliability of the service for extended and scheduled requests. If the query parameter 'detail=min' is used, a light weight heart beat test will be performed. The heart beat test returns status 200 if the service is available.
+ parameters:
+ - name: detail
+ in: query
+ description: specifies heart beat to be used to check for availability of this service, the value 'min' must be used, otherwise the full availability test will be performed
+ required: false
+ type: string
+ responses:
+ '200':
+ description: A VOSI availability document in XML.
+ schema:
+ $ref: '#/definitions/availability'
+ default:
+ description: Unexpected error
+ schema:
+ $ref: '#/definitions/Error'
+ /capabilities:
+ get:
+ summary: VOSI Capabilities
+ tags:
+ - Support Interfaces
+ description: |
+ Provides the service metadata in the form of a list of Capability descriptions. Each of these descriptions is an
+ XML element that:
+
+ - states that the service provides a particular, IVOA-standard function;
+ - lists the interfaces for invoking that function;
+ - records any details of the implementation of the function that are not defined as default or constant in the standard for that function.
+
+ responses:
+ '200':
+ description: A VOSI Capabilities document in XML.
+ schema:
+ $ref: '#/definitions/capabilities'
+ '500':
+ description: Internal server error
+ '503':
+ description: Service too busy
+ default:
+ description: Unexpected error
+ schema:
+ $ref: '#/definitions/Error'
+definitions:
+ Property:
+ type: object
+ required:
+ - uri
+ description: The property identifier
+ properties:
+ uri:
+ type: string
+ format: uri
+ View:
+ type: object
+ required:
+ - uri
+ description: The view identifier
+ properties:
+ uri:
+ type: string
+ format: uri
+ Protocol:
+ type: object
+ required:
+ - uri
+ description: The protocol identifier
+ properties:
+ uri:
+ type: string
+ format: uri
+ Node:
+ type: object
+ required:
+ - uri
+ description: The node identifier
+ properties:
+ uri:
+ type: string
+ format: uri
+ Transfer:
+ type: object
+ required:
+ - target
+ - direction
+ - protocol
+ description: The transfer negotiation document
+ properties:
+ target:
+ type: string
+ format: uri
+ direction:
+ type: string
+ protocol:
+ type: string
+ format: uri
+ availability:
+ type: object
+ xml:
+ name: availability
+ namespace: http://www.ivoa.net/xml/VOSIAvailability/v1.0
+ prefix: vosi
+ properties:
+ available:
+ type: boolean
+ xml:
+ attribute: true
+ prefix: vosi
+ note:
+ type: string
+ xml:
+ attribute: true
+ prefix: vosi
+ capabilities:
+ type: array
+ items:
+ $ref: '#/definitions/capability'
+ xml:
+ namespace: http://www.ivoa.net/xml/VOSICapabilities/v1.0
+ prefix: vosi
+ wrapped: true
+ capability:
+ type: object
+ properties:
+ standardID:
+ type: string
+ format: uri
+ xml:
+ attribute: true
+ interface:
+ type: object
+ properties:
+ accessURL:
+ type: string
+ properties:
+ use:
+ type: string
+ xml:
+ attribute: true
+ securityMethod:
+ type: string
+ properties:
+ standardID:
+ type: string
+ format: uri
+ xml:
+ attribute: true
+ Error:
+ required:
+ - code
+ - message
+ properties:
+ code:
+ type: integer
+ format: int32
+ message:
+ type: string