diff --git a/src/main/java/it/inaf/oats/vospace/JobService.java b/src/main/java/it/inaf/oats/vospace/JobService.java
index 2f397b160636a428ec3ead255e9b4ac0a2d3a128..95e11ada1c54b48f296b0f6dcbbf22daec66fade 100644
--- a/src/main/java/it/inaf/oats/vospace/JobService.java
+++ b/src/main/java/it/inaf/oats/vospace/JobService.java
@@ -1,8 +1,10 @@
 package it.inaf.oats.vospace;
 
+import it.inaf.oats.vospace.exception.InternalFaultException;
 import it.inaf.oats.vospace.persistence.JobDAO;
 import net.ivoa.xml.uws.v1.ExecutionPhase;
 import net.ivoa.xml.uws.v1.JobSummary;
+import net.ivoa.xml.vospace.v2.Protocol;
 import net.ivoa.xml.vospace.v2.Transfer;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -43,35 +45,48 @@ public class JobService {
 
     private void startJob(JobSummary job) {
 
-        switch (getJobType(job)) {
+        Transfer transfer = uriService.getTransfer(job);
+
+        switch (getJobType(transfer)) {
             case pullToVoSpace:
-                handlePullToVoSpace(job);
+                handlePullToVoSpace(job, transfer);
                 break;
             case pullFromVoSpace:
             case pushToVoSpace:
-                handleVoSpaceUrlsListResult(job);
+                handleVoSpaceUrlsListResult(job, transfer);
                 break;
             default:
                 throw new UnsupportedOperationException("Not implemented yet");
         }
     }
 
-    private void handlePullToVoSpace(JobSummary job) {
-        // TODO: check protocol
-        asyncTransfService.startJob(job);
+    private void handlePullToVoSpace(JobSummary job, Transfer transfer) {
+
+        for (Protocol protocol : transfer.getProtocols()) {
+            switch (protocol.getUri()) {
+                case "ia2:async-recall":
+                    asyncTransfService.startJob(job);
+                    return;
+                case "ivo://ivoa.net/vospace/core#httpget":
+                    String nodeUri = transfer.getTarget();
+                    String contentUri = protocol.getEndpoint();
+                    uriService.setNodeRemoteLocation(nodeUri, contentUri);
+                    uriService.setTransferJobResult(job, transfer);
+                    jobDAO.updateJob(job);
+                    return;
+                default:
+                    throw new InternalFaultException("Unsupported pullToVoSpace protocol: " + protocol.getUri());
+            }
+        }
     }
 
-    private void handleVoSpaceUrlsListResult(JobSummary job) {
+    private void handleVoSpaceUrlsListResult(JobSummary job, Transfer transfer) {
         job.setPhase(ExecutionPhase.EXECUTING);
-        uriService.setTransferJobResult(job);
+        uriService.setTransferJobResult(job, transfer);
         jobDAO.updateJob(job);
     }
 
-    private JobType getJobType(JobSummary job) {
-
-        // TODO: check types
-        Transfer transfer = (Transfer) job.getJobInfo().getAny().get(0);
-
+    private JobType getJobType(Transfer transfer) {
         return JobType.valueOf(transfer.getDirection());
     }
 
diff --git a/src/main/java/it/inaf/oats/vospace/UriService.java b/src/main/java/it/inaf/oats/vospace/UriService.java
index f95a87e7f9ef0a56cd407fab609667952d6ca88c..5e80e132d94fb0e8dcabf312803bdfb0263521d4 100644
--- a/src/main/java/it/inaf/oats/vospace/UriService.java
+++ b/src/main/java/it/inaf/oats/vospace/UriService.java
@@ -5,11 +5,18 @@ import it.inaf.ia2.aa.data.User;
 import it.inaf.ia2.rap.client.call.TokenExchangeRequest;
 import it.inaf.oats.vospace.datamodel.NodeProperties;
 import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
+import it.inaf.oats.vospace.exception.InternalFaultException;
 import it.inaf.oats.vospace.exception.NodeNotFoundException;
+import it.inaf.oats.vospace.exception.PermissionDeniedException;
+import it.inaf.oats.vospace.persistence.LocationDAO;
 import it.inaf.oats.vospace.persistence.NodeDAO;
+import it.inaf.oats.vospace.persistence.model.Location;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
 import javax.servlet.http.HttpServletRequest;
+import net.ivoa.xml.uws.v1.ExecutionPhase;
 import net.ivoa.xml.uws.v1.JobSummary;
 import net.ivoa.xml.uws.v1.ResultReference;
 import net.ivoa.xml.vospace.v2.Node;
@@ -30,6 +37,9 @@ public class UriService {
 
     @Autowired
     private NodeDAO nodeDao;
+    
+    @Autowired
+    private LocationDAO locationDAO;
 
     @Autowired
     private HttpServletRequest servletRequest;
@@ -37,15 +47,16 @@ public class UriService {
     @Autowired
     private ServletRapClient rapClient;
 
-    public void setTransferJobResult(JobSummary job) {
+    public void setTransferJobResult(JobSummary job, Transfer transfer) {
 
         List<ResultReference> results = new ArrayList<>();
 
         ResultReference result = new ResultReference();
-        result.setHref(getEndpoint(job));
+        result.setHref(getEndpoint(job, transfer));
         results.add(result);
 
         job.setResults(results);
+        job.setPhase(ExecutionPhase.COMPLETED);
     }
 
     public void setSyncTransferEndpoints(JobSummary job) {
@@ -58,20 +69,33 @@ public class UriService {
                 && !"ivo://ivoa.net/vospace/core#httpput".equals(protocol.getUri())) {
             throw new IllegalStateException("Unsupported protocol " + protocol.getUri());
         }
-        protocol.setEndpoint(getEndpoint(job));
+        protocol.setEndpoint(getEndpoint(job, transfer));
     }
 
-    private String getEndpoint(JobSummary job) {
-
-        Transfer transfer = getTransfer(job);
+    private String getEndpoint(JobSummary job, Transfer transfer) {
 
         String relativePath = transfer.getTarget().substring("vos://".length() + authority.length());
 
         Node node = nodeDao.listNode(relativePath).orElseThrow(() -> new NodeNotFoundException(relativePath));
 
-        // TODO build the path according to node type
-        //
-        String endpoint = fileServiceUrl + urlEncodePath(relativePath) + "?jobId=" + job.getJobId();
+        Location location = locationDAO.getNodeLocation(relativePath).orElseThrow(()
+                -> new InternalFaultException("No registered location found for vos_path " + relativePath));
+
+        String endpoint;
+        switch (location.getType()) {
+            case PORTAL:
+                String fileName = nodeDao.getNodeOsName(relativePath);
+                endpoint = "http://" + location.getSource().getHostname() + location.getSource().getBasePath();
+                if (!endpoint.endsWith("/")) {
+                    endpoint += "/";
+                }
+                endpoint += fileName;
+                break;
+            default:
+                endpoint = fileServiceUrl + urlEncodePath(relativePath);
+        }
+
+        endpoint += "?jobId=" + job.getJobId();
 
         if (!"true".equals(NodeProperties.getNodePropertyByURI(node, NodeProperties.PUBLIC_READ_URI))) {
             endpoint += "&token=" + getEndpointToken(fileServiceUrl + relativePath);
@@ -85,8 +109,7 @@ public class UriService {
         String token = ((User) servletRequest.getUserPrincipal()).getAccessToken();
 
         if (token == null) {
-            // TODO: use PermissionDenied VoSpaceException
-            throw new IllegalStateException("Token is null");
+            throw new PermissionDeniedException("Token is null");
         }
 
         TokenExchangeRequest exchangeRequest = new TokenExchangeRequest()
@@ -97,8 +120,39 @@ public class UriService {
         return rapClient.exchangeToken(exchangeRequest, servletRequest);
     }
 
-    private Transfer getTransfer(JobSummary job) {
-        // TODO add checks on data type
+    public void setNodeRemoteLocation(String nodeUri, String contentUri) {
+
+        URL url;
+        try {
+            url = new URL(contentUri);
+        } catch (MalformedURLException ex) {
+            throw new InternalFaultException(ex);
+        }
+
+        Location location = locationDAO.findPortalLocation(url.getHost()).orElseThrow(()
+                -> new InternalFaultException("No registered location found for host " + url.getHost()));
+
+        String vosPath = nodeUri.replaceAll("vos://[^/]+", "");
+
+        String fileName = url.getPath().substring(url.getPath().lastIndexOf("/") + 1);
+
+        nodeDao.setNodeLocation(vosPath, location.getId(), fileName);
+    }
+
+    public Transfer getTransfer(JobSummary job) {
+
+        List<Object> jobPayload = job.getJobInfo().getAny();
+        if (jobPayload.isEmpty()) {
+            throw new IllegalStateException("Empty job payload for job " + job.getJobId());
+        }
+        if (jobPayload.size() > 1) {
+            throw new IllegalStateException("Multiple objects in job payload not supported");
+        }
+        if (!(jobPayload.get(0) instanceof Transfer)) {
+            throw new IllegalStateException(jobPayload.get(0).getClass().getCanonicalName()
+                    + " not supported as job payload. Job id: " + job.getJobId());
+        }
+
         return (Transfer) job.getJobInfo().getAny().get(0);
     }
 }
diff --git a/src/main/java/it/inaf/oats/vospace/exception/InternalFaultException.java b/src/main/java/it/inaf/oats/vospace/exception/InternalFaultException.java
index 2429c070a061a09f10ea8eb712c1993847dcd1f8..635bf575bdbf5ec2d48c58fec7547346f9eebfa7 100644
--- a/src/main/java/it/inaf/oats/vospace/exception/InternalFaultException.java
+++ b/src/main/java/it/inaf/oats/vospace/exception/InternalFaultException.java
@@ -1,19 +1,25 @@
-/*
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
- */
 package it.inaf.oats.vospace.exception;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.ResponseStatus;
 
-
 @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)   // Status code 500
-public class InternalFaultException  extends VoSpaceException {
+public class InternalFaultException extends VoSpaceException {
+
+    private static final Logger LOG = LoggerFactory.getLogger(InternalFaultException.class);
 
     public InternalFaultException(String msg) {
         super("InternalFaultException: " + msg);
     }
-    
+
+    public InternalFaultException(Throwable cause) {
+        super("InternalFaultException: " + getMessage(cause));
+    }
+
+    private static String getMessage(Throwable cause) {
+        LOG.error("Exception caught", cause);
+        return cause.getMessage() != null ? cause.getMessage() : cause.getClass().getCanonicalName();
+    }
 }
diff --git a/src/main/java/it/inaf/oats/vospace/persistence/LocationDAO.java b/src/main/java/it/inaf/oats/vospace/persistence/LocationDAO.java
new file mode 100644
index 0000000000000000000000000000000000000000..ef8618c646bfc08b45de78ea5bdc50d3294a5202
--- /dev/null
+++ b/src/main/java/it/inaf/oats/vospace/persistence/LocationDAO.java
@@ -0,0 +1,113 @@
+package it.inaf.oats.vospace.persistence;
+
+import it.inaf.oats.vospace.persistence.model.Location;
+import it.inaf.oats.vospace.persistence.model.LocationType;
+import it.inaf.oats.vospace.persistence.model.Storage;
+import it.inaf.oats.vospace.persistence.model.StorageType;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+import javax.sql.DataSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class LocationDAO {
+
+    private final JdbcTemplate jdbcTemplate;
+
+    @Autowired
+    public LocationDAO(DataSource dataSource) {
+        jdbcTemplate = new JdbcTemplate(dataSource);
+    }
+
+    public Optional<Location> findPortalLocation(String host) {
+
+        String sql = "SELECT location_id, location_type, storage_src_id, storage_dest_id,\n"
+                + "storage_id, storage_type, base_path, hostname\n"
+                + "FROM location l\n"
+                + "JOIN storage s ON l.storage_src_id = s.storage_id OR l.storage_dest_id = s.storage_id\n"
+                + "WHERE hostname = ?";
+
+        return jdbcTemplate.query(sql, ps -> {
+            ps.setString(1, host);
+        }, rs -> {
+            return getLocation(rs, () -> new IllegalStateException("Found multiple locations for the same hostname"));
+        });
+    }
+
+    public Optional<Location> getNodeLocation(String vosPath) {
+
+        String sql = "SELECT location_id, location_type, storage_src_id, storage_dest_id,\n"
+                + "storage_id, storage_type, base_path, hostname\n"
+                + "FROM location l\n"
+                + "JOIN storage s ON l.storage_src_id = s.storage_id OR l.storage_dest_id = s.storage_id\n"
+                + "WHERE location_id = (\n"
+                + "SELECT location_id FROM node n\n"
+                + "JOIN node_vos_path p ON n.node_id = p.node_id\n"
+                + "WHERE p.vos_path = ?)";
+
+        return jdbcTemplate.query(sql, ps -> {
+            ps.setString(1, vosPath);
+        }, rs -> {
+            return getLocation(rs, () -> new IllegalStateException("Found multiple locations for the same vos_path"));
+        });
+    }
+
+    private Optional<Location> getLocation(ResultSet rs, Supplier<IllegalStateException> exceptionSupplier) throws SQLException {
+        List<Location> locations = getLocations(rs);
+        if (locations.isEmpty()) {
+            return Optional.empty();
+        }
+        if (locations.size() > 1) {
+            throw exceptionSupplier.get();
+        }
+        return Optional.of(locations.get(0));
+    }
+
+    private List<Location> getLocations(ResultSet rs) throws SQLException {
+
+        Map<Integer, Storage> storagesMap = new HashMap<>();
+        Map<Integer, Location> locationsMap = new HashMap<>();
+
+        while (rs.next()) {
+            int locationId = rs.getInt("location_id");
+            Location location = locationsMap.get(locationId);
+            if (location == null) {
+                location = new Location();
+                location.setId(locationId);
+                locationsMap.put(locationId, location);
+            }
+            location.setType(LocationType.parse(rs.getString("location_type")));
+
+            int storageId = rs.getInt("storage_id");
+            Storage storage = storagesMap.get(storageId);
+            if (storage == null) {
+                storage = new Storage();
+                storage.setId(storageId);
+                storagesMap.put(storageId, storage);
+            }
+
+            storage.setType(StorageType.parse(rs.getString("storage_type")));
+            storage.setBasePath(rs.getString("base_path"));
+            storage.setHostname(rs.getString("hostname"));
+
+            Storage storageSrc = storagesMap.get(rs.getInt("storage_src_id"));
+            if (storageSrc != null) {
+                location.setSource(storageSrc);
+            }
+            Storage storageDest = storagesMap.get(rs.getInt("storage_dest_id"));
+            if (storageDest != null) {
+                location.setDestination(storageDest);
+            }
+        }
+
+        return new ArrayList<>(locationsMap.values());
+    }
+}
diff --git a/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java b/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java
index 216f64c9264dcdae970c28672da39e0686e338b8..9267722d57563ecbe790ea86d12291337b794c98 100644
--- a/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java
+++ b/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java
@@ -1,5 +1,7 @@
 package it.inaf.oats.vospace.persistence;
 
+import it.inaf.oats.vospace.datamodel.NodeUtils;
+import it.inaf.oats.vospace.exception.InternalFaultException;
 import java.sql.Array;
 import net.ivoa.xml.vospace.v2.Node;
 import java.sql.PreparedStatement;
@@ -239,6 +241,35 @@ public class NodeDAO {
 
         return jdbcTemplate.queryForObject(sql, args, types, Integer.class);
     }
+    
+    public String getNodeOsName(String vosPath) {
+        String sql = "SELECT \n"
+                + "(CASE WHEN os_name IS NOT NULL THEN os_name ELSE name END) AS os_name\n"
+                + "FROM node n\n"
+                + "JOIN node_vos_path p ON n.node_id = p.node_id\n"
+                + "WHERE p.vos_path = ?";
+
+        Object[] args = {vosPath};
+        int[] types = {Types.VARCHAR};
+
+        return jdbcTemplate.queryForObject(sql, args, types, String.class);
+    }
+    
+    public void setNodeLocation(String vosPath, int locationId, String nodeOsName) {
+
+        String sql = "UPDATE node SET location_id = ?, os_name = ? WHERE node_id = "
+                + "(SELECT node_id FROM node_vos_path WHERE vos_path = ?)";
+
+        int updated = jdbcTemplate.update(sql, ps -> {
+            ps.setInt(1, locationId);
+            ps.setString(2, nodeOsName);
+            ps.setString(3, vosPath);
+        });
+
+        if (updated != 1) {
+            throw new InternalFaultException("Unable to set node location for path " + vosPath);
+        }
+    }
 
     private String getPropertyURI(String propertyName) {
         return "ivo://ivoa.net/vospace/core#".concat(propertyName);
diff --git a/src/main/java/it/inaf/oats/vospace/persistence/model/Location.java b/src/main/java/it/inaf/oats/vospace/persistence/model/Location.java
new file mode 100644
index 0000000000000000000000000000000000000000..3d6b2814ffaa35c1997c956a323020f838d46d6f
--- /dev/null
+++ b/src/main/java/it/inaf/oats/vospace/persistence/model/Location.java
@@ -0,0 +1,41 @@
+package it.inaf.oats.vospace.persistence.model;
+
+public class Location {
+
+    private int id;
+    private LocationType type;
+    private Storage source;
+    private Storage destination;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public LocationType getType() {
+        return type;
+    }
+
+    public void setType(LocationType type) {
+        this.type = type;
+    }
+
+    public Storage getSource() {
+        return source;
+    }
+
+    public void setSource(Storage source) {
+        this.source = source;
+    }
+
+    public Storage getDestination() {
+        return destination;
+    }
+
+    public void setDestination(Storage destination) {
+        this.destination = destination;
+    }
+}
diff --git a/src/main/java/it/inaf/oats/vospace/persistence/model/LocationType.java b/src/main/java/it/inaf/oats/vospace/persistence/model/LocationType.java
new file mode 100644
index 0000000000000000000000000000000000000000..d64a231705c5763bef41f7b160d5e6cc13ce179b
--- /dev/null
+++ b/src/main/java/it/inaf/oats/vospace/persistence/model/LocationType.java
@@ -0,0 +1,28 @@
+package it.inaf.oats.vospace.persistence.model;
+
+public enum LocationType {
+
+    ASYNC("async"),
+    PORTAL("portal"),
+    USER("user");
+
+    private final String name;
+
+    private LocationType(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    public static LocationType parse(String value) {
+        for (LocationType type : LocationType.values()) {
+            if (type.name.equals(value)) {
+                return type;
+            }
+        }
+        throw new IllegalArgumentException("Invalid LocationType " + value);
+    }
+}
diff --git a/src/main/java/it/inaf/oats/vospace/persistence/model/Storage.java b/src/main/java/it/inaf/oats/vospace/persistence/model/Storage.java
new file mode 100644
index 0000000000000000000000000000000000000000..f936ac95aca3fc447c8dbfaa46afceeb42968696
--- /dev/null
+++ b/src/main/java/it/inaf/oats/vospace/persistence/model/Storage.java
@@ -0,0 +1,41 @@
+package it.inaf.oats.vospace.persistence.model;
+
+public class Storage {
+
+    private int id;
+    private StorageType type;
+    private String basePath;
+    private String hostname;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public StorageType getType() {
+        return type;
+    }
+
+    public void setType(StorageType type) {
+        this.type = type;
+    }
+
+    public String getBasePath() {
+        return basePath;
+    }
+
+    public void setBasePath(String basePath) {
+        this.basePath = basePath;
+    }
+
+    public String getHostname() {
+        return hostname;
+    }
+
+    public void setHostname(String hostname) {
+        this.hostname = hostname;
+    }
+}
diff --git a/src/main/java/it/inaf/oats/vospace/persistence/model/StorageType.java b/src/main/java/it/inaf/oats/vospace/persistence/model/StorageType.java
new file mode 100644
index 0000000000000000000000000000000000000000..65b07c2f7fdf29365ba1dbadc5e24ce95e6c0af7
--- /dev/null
+++ b/src/main/java/it/inaf/oats/vospace/persistence/model/StorageType.java
@@ -0,0 +1,29 @@
+package it.inaf.oats.vospace.persistence.model;
+
+public enum StorageType {
+
+    COLD("cold"),
+    HOT("hot"),
+    LOCAL("local"),
+    PORTAL("portal");
+
+    private final String name;
+
+    private StorageType(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    public static StorageType parse(String value) {
+        for (StorageType type : StorageType.values()) {
+            if (type.name.equals(value)) {
+                return type;
+            }
+        }
+        throw new IllegalArgumentException("Invalid StorageType " + value);
+    }
+}
diff --git a/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java b/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java
index 5dd3cb6af781e1cdb6ea77f18bd4af62ce12842c..a15c8811ff056214080ff00c05bec9218db79539 100644
--- a/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java
+++ b/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java
@@ -3,7 +3,11 @@ package it.inaf.oats.vospace;
 import it.inaf.ia2.aa.data.User;
 import static it.inaf.oats.vospace.VOSpaceXmlTestUtil.loadDocument;
 import it.inaf.oats.vospace.persistence.JobDAO;
+import it.inaf.oats.vospace.persistence.LocationDAO;
 import it.inaf.oats.vospace.persistence.NodeDAO;
+import it.inaf.oats.vospace.persistence.model.Location;
+import it.inaf.oats.vospace.persistence.model.LocationType;
+import it.inaf.oats.vospace.persistence.model.Storage;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.sql.Timestamp;
@@ -41,6 +45,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 import org.w3c.dom.Document;
 import java.util.List;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.BeforeEach;
+import static org.mockito.ArgumentMatchers.argThat;
 
 @SpringBootTest
 @AutoConfigureMockMvc
@@ -55,11 +62,32 @@ public class TransferControllerTest {
     private NodeDAO nodeDao;
 
     @MockBean
-    private AsyncTransferService tapeService;
+    private LocationDAO locationDao;
+
+    @MockBean
+    private AsyncTransferService asyncTransfService;
 
     @Autowired
     private MockMvc mockMvc;
 
+    @BeforeEach
+    public void init() {
+        Location asyncLocation = new Location();
+        asyncLocation.setType(LocationType.ASYNC);
+        asyncLocation.setId(1);
+        when(locationDao.getNodeLocation(eq("/mynode"))).thenReturn(Optional.of(asyncLocation));
+
+        Location portalLocation = new Location();
+        portalLocation.setType(LocationType.PORTAL);
+        portalLocation.setId(2);
+        Storage portalStorage = new Storage();
+        portalStorage.setHostname("archive.lbto.org");
+        portalStorage.setBasePath("/files");
+        portalLocation.setSource(portalStorage);
+        when(locationDao.getNodeLocation(eq("/portalnode"))).thenReturn(Optional.of(portalLocation));
+        when(locationDao.findPortalLocation(any())).thenReturn(Optional.of(portalLocation));
+    }
+
     @Test
     public void testPullFromVoSpaceAsync() throws Exception {
 
@@ -99,9 +127,32 @@ public class TransferControllerTest {
     }
 
     @Test
-    public void testPullToVoSpace() throws Exception {
+    public void testPullToVoSpaceTape() throws Exception {
+        testPullToVoSpace("/mynode", getResourceFileContent("pullToVoSpace-tape.xml"));
+
+        verify(asyncTransfService, times(1)).startJob(any());
+    }
+
+    @Test
+    public void testPullToVoSpacePortal() throws Exception {
+
+        when(nodeDao.getNodeOsName(eq("/portalnode"))).thenReturn("file.fits");
+
+        testPullToVoSpace("/portalnode", getResourceFileContent("pullToVoSpace-portal.xml"));
+
+        verify(nodeDao, times(1)).setNodeLocation(eq("/portalnode"), eq(2), eq("lbcr.20130512.060722.fits.gz"));
+        
+        verify(jobDao, times(1)).updateJob(argThat(j -> {
+            assertTrue(j.getResults().get(0).getHref().startsWith("http://archive.lbto.org"));
+            assertEquals(ExecutionPhase.COMPLETED, j.getPhase());
+            return true;
+        }));
+    }
+
+    private void testPullToVoSpace(String path, String requestBody) throws Exception {
 
-        String requestBody = getResourceFileContent("pullToVoSpace.xml");
+        Node node = mockPublicDataNode();
+        when(nodeDao.listNode(eq(path))).thenReturn(Optional.of(node));
 
         String redirect = mockMvc.perform(post("/transfers?PHASE=RUN")
                 .content(requestBody)
@@ -112,8 +163,6 @@ public class TransferControllerTest {
                 .andReturn().getResponse().getHeader("Location");
 
         assertThat(redirect, matchesPattern("^/transfers/.*"));
-
-        verify(tapeService, times(1)).startJob(any());
     }
 
     @Test
@@ -196,24 +245,23 @@ public class TransferControllerTest {
 
         verify(jobDao, times(1)).getJob(eq("123"));
     }
-    
+
     @Test
     public void testGetJobs() throws Exception {
-       
+
         when(jobDao.getJobs(eq("user1"), any(), any(), any(), any()))
                 .thenReturn(this.getFakeJobs());
-        
+
         mockMvc.perform(get("/transfers")
                 .header("Authorization", "Bearer user1_token")
                 .param("LAST", "-3")
-                .accept(MediaType.APPLICATION_XML))                
+                .accept(MediaType.APPLICATION_XML))
                 .andDo(print())
                 .andExpect(status().is4xxClientError());
-                                
-        
+
         String xml2 = mockMvc.perform(get("/transfers")
                 .header("Authorization", "Bearer user1_token")
-                .accept(MediaType.APPLICATION_XML))                
+                .accept(MediaType.APPLICATION_XML))
                 .andDo(print())
                 .andExpect(status().isOk())
                 .andReturn().getResponse().getContentAsString();
diff --git a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java
index b50eda77988ef4ed2c4b466e080c5637257393a2..817d13b4e710fbf2679f85f4e47bc1c53cb9d147 100644
--- a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java
+++ b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java
@@ -2,7 +2,10 @@ package it.inaf.oats.vospace;
 
 import it.inaf.ia2.aa.ServletRapClient;
 import it.inaf.ia2.aa.data.User;
+import it.inaf.oats.vospace.persistence.LocationDAO;
 import it.inaf.oats.vospace.persistence.NodeDAO;
+import it.inaf.oats.vospace.persistence.model.Location;
+import it.inaf.oats.vospace.persistence.model.LocationType;
 import java.util.Optional;
 import javax.servlet.http.HttpServletRequest;
 import net.ivoa.xml.uws.v1.JobSummary;
@@ -11,11 +14,13 @@ import net.ivoa.xml.vospace.v2.Node;
 import net.ivoa.xml.vospace.v2.Property;
 import net.ivoa.xml.vospace.v2.Transfer;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
@@ -32,8 +37,11 @@ import org.springframework.test.context.TestPropertySource;
 public class UriServiceTest {
 
     @MockBean
-    private NodeDAO dao;
+    private NodeDAO nodeDAO;
 
+    @MockBean
+    private LocationDAO locationDAO;
+    
     @MockBean
     private ServletRapClient rapClient;
 
@@ -55,6 +63,13 @@ public class UriServiceTest {
             return mock(HttpServletRequest.class);
         }
     }
+    
+    @BeforeEach
+    public void init() {
+        Location location = new Location();
+        location.setType(LocationType.ASYNC);
+        when(locationDAO.getNodeLocation(any())).thenReturn(Optional.of(location));
+    }
 
     @Test
     public void testPublicUrl() {
@@ -65,10 +80,10 @@ public class UriServiceTest {
         property.setValue("true");
         node.getProperties().add(property);
 
-        when(dao.listNode(eq("/mydata1"))).thenReturn(Optional.of(node));
+        when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node));
 
         JobSummary job = getJob();
-        uriService.setTransferJobResult(job);
+        uriService.setTransferJobResult(job, uriService.getTransfer(job));
 
         assertEquals("http://file-service/mydata1?jobId=job-id", job.getResults().get(0).getHref());
     }
@@ -78,7 +93,7 @@ public class UriServiceTest {
 
         Node node = new DataNode();
 
-        when(dao.listNode(eq("/mydata1"))).thenReturn(Optional.of(node));
+        when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node));
 
         User user = mock(User.class);
         when(user.getAccessToken()).thenReturn("<token>");
@@ -92,11 +107,27 @@ public class UriServiceTest {
         }), any())).thenReturn("<new-token>");
 
         JobSummary job = getJob();
-        uriService.setTransferJobResult(job);
+        uriService.setTransferJobResult(job, uriService.getTransfer(job));
 
         assertEquals("http://file-service/mydata1?jobId=job-id&token=<new-token>", job.getResults().get(0).getHref());
     }
 
+    @Test
+    public void setNodeRemoteLocationTest() {
+
+        String nodeUri = "vos://example.com!vospace/test/f1/lbtfile.fits";
+        String contentUri = "http://archive.lbto.org/files/lbtfile.fits";
+
+        Location location = new Location();
+        location.setId(5);
+
+        when(locationDAO.findPortalLocation(eq("archive.lbto.org"))).thenReturn(Optional.of(location));
+
+        uriService.setNodeRemoteLocation(nodeUri, contentUri);
+
+        verify(nodeDAO).setNodeLocation(eq("/test/f1/lbtfile.fits"), eq(5), eq("lbtfile.fits"));
+    }
+    
     private JobSummary getJob() {
 
         Transfer transfer = new Transfer();
diff --git a/src/test/java/it/inaf/oats/vospace/persistence/LocationDAOTest.java b/src/test/java/it/inaf/oats/vospace/persistence/LocationDAOTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..067c2e6ba429437f370e9feffb4a5b33613ffe87
--- /dev/null
+++ b/src/test/java/it/inaf/oats/vospace/persistence/LocationDAOTest.java
@@ -0,0 +1,52 @@
+package it.inaf.oats.vospace.persistence;
+
+import it.inaf.oats.vospace.persistence.model.Location;
+import java.util.Optional;
+import javax.sql.DataSource;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = {DataSourceConfig.class})
+@TestPropertySource(locations = "classpath:test.properties")
+public class LocationDAOTest {
+
+    @Autowired
+    private DataSource dataSource;
+    private LocationDAO dao;
+
+    @BeforeEach
+    public void init() {
+        dao = new LocationDAO(dataSource);
+    }
+
+    @Test
+    public void testGetPortalLocation() {
+
+        String hostname = "archive.lbto.org";
+
+        Optional<Location> optLocation = dao.findPortalLocation(hostname);
+        assertTrue(optLocation.isPresent());
+
+        Location location = optLocation.get();
+
+        assertEquals(hostname, location.getSource().getHostname());
+    }
+
+    @Test
+    public void testInexistentLocation() {
+        assertTrue(dao.findPortalLocation("foo").isEmpty());
+    }
+
+    @Test
+    public void testGetRootLocation() {
+        dao.getNodeLocation("/");
+    }
+}
diff --git a/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java b/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java
index 073e2646f0058c1e209e6f67f290c6b9fc52f040..eb27450df9117773d25175b58b150fc9abddb82c 100644
--- a/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java
+++ b/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java
@@ -1,7 +1,7 @@
 package it.inaf.oats.vospace.persistence;
 
 import it.inaf.oats.vospace.datamodel.NodeProperties;
-import it.inaf.oats.vospace.datamodel.NodeUtils;
+import it.inaf.oats.vospace.exception.InternalFaultException;
 import java.util.ArrayList;
 import java.util.List;
 import javax.sql.DataSource;
@@ -89,6 +89,27 @@ public class NodeDAOTest {
         assertEquals(0, dao.countNodesWithPath("/test1/f1/f2_renamed/f3"));
 
     }
+    
+    @Test
+    public void testSetNodeLocation() {
+
+        DataNode dataNode = new DataNode();
+        dataNode.setUri("vos://example.com!vospace/mydata2");
+        dao.createNode(dataNode);
+
+        dao.setNodeLocation("/mydata2", 1, "mydata2");
+    }
+    
+    @Test
+    public void testSetNodeLocationFailure() {
+        boolean exception = false;
+        try {
+            dao.setNodeLocation("/foo", 1, "foo");
+        } catch (InternalFaultException ex) {
+            exception = true;
+        }
+        assertTrue(exception);
+    }
 
     private String getProperty(Node node, String uri) {
         for (Property property : node.getProperties()) {
diff --git a/src/test/resources/pullToVoSpace-portal.xml b/src/test/resources/pullToVoSpace-portal.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f83a08136da420a523657939a102367828f95718
--- /dev/null
+++ b/src/test/resources/pullToVoSpace-portal.xml
@@ -0,0 +1,7 @@
+<vos:transfer xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" version="2.1">
+    <vos:target>vos://example.com!vospace/portalnode</vos:target>
+    <vos:direction>pullToVoSpace</vos:direction>
+    <vos:protocol uri="ivo://ivoa.net/vospace/core#httpget">
+        <vos:endpoint>http://archive.lbto.org/files/lbcr.20130512.060722.fits.gz</vos:endpoint>
+    </vos:protocol>
+</vos:transfer>
\ No newline at end of file
diff --git a/src/test/resources/pullToVoSpace.xml b/src/test/resources/pullToVoSpace-tape.xml
similarity index 76%
rename from src/test/resources/pullToVoSpace.xml
rename to src/test/resources/pullToVoSpace-tape.xml
index 8673c57e68c120f52a1b92b7988b3835cb3c4d51..51e99e9f8218a90b89f960bf4173e5e598e4b577 100644
--- a/src/test/resources/pullToVoSpace.xml
+++ b/src/test/resources/pullToVoSpace-tape.xml
@@ -1,5 +1,5 @@
 <vos:transfer xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" version="2.1">
     <vos:target>vos://example.com!vospace/mynode</vos:target>
     <vos:direction>pullToVoSpace</vos:direction>
-    <vos:protocol uri="ivo://ivoa.net/vospace/core#httpget" />
+    <vos:protocol uri="ia2:async-recall" />
 </vos:transfer>
\ No newline at end of file