diff --git a/include/caosdb/entity.h b/include/caosdb/entity.h
index eb93b36875bfe7778345152bcbb53777d9509759..67dd71a563caa9d8c0372542e92f3b8a971b3b0e 100644
--- a/include/caosdb/entity.h
+++ b/include/caosdb/entity.h
@@ -33,6 +33,7 @@
 #include "caosdb/entity/v1alpha1/main.pb.h" // for RepeatedPtrField, Message
 #include "caosdb/message_code.h"            // for get_message_code, Messag...
 #include "google/protobuf/util/json_util.h" // for MessageToJsonString, Jso...
+#include "boost/filesystem/path.hpp"        // for path
 
 namespace caosdb::entity {
 using caosdb::entity::v1alpha1::IdResponse;
@@ -41,6 +42,20 @@ using ProtoProperty = caosdb::entity::v1alpha1::Property;
 using ProtoEntity = caosdb::entity::v1alpha1::Entity;
 using caosdb::entity::v1alpha1::FileTransmissionId;
 
+class FileDescriptor {
+public:
+  auto GetEntityId() const -> const std::string &;
+  auto GetLocalPath() const -> const boost::filesystem::path &;
+  auto GetRemotePath() const -> const std::string &;
+  auto GetSize() const -> long long;
+
+private:
+  std::string entity_id;
+  std::string remote_path;
+  std::string local_path;
+  long long size;
+};
+
 /**
  * Messages convey information about the state and result of transactions.
  *
diff --git a/include/caosdb/transaction.h b/include/caosdb/transaction.h
index 1b89ecbec2c4e7c15bbe9739809ee0ee1af04cac..a6d2be0616e08ef346a88e2d258c812616f24af0 100644
--- a/include/caosdb/transaction.h
+++ b/include/caosdb/transaction.h
@@ -33,10 +33,11 @@
 #include "google/protobuf/util/json_util.h" // for MessageToJsonString, Jso...
 #include <iterator>
 // IWYU pragma: no_include <ext/alloc_traits.h>
-#include <memory> // for shared_ptr, unique_ptr
-#include <stdexcept>
-#include <string> // for string
-#include <vector> // for vector
+#include <memory>    // for shared_ptr, unique_ptr
+#include <stdexcept> // for out_of_range
+#include <string>    // for string
+#include <utility>   // for move
+#include <vector>    // for vector
 
 /**
  * Do all necessary checks and assure that another retrieval (by id or by
@@ -158,6 +159,7 @@
  */
 namespace caosdb::transaction {
 using caosdb::entity::Entity;
+using caosdb::entity::FileDescriptor;
 using ProtoEntity = caosdb::entity::v1alpha1::Entity;
 using caosdb::entity::v1alpha1::EntityTransactionService;
 using caosdb::entity::v1alpha1::FileDownloadResponse;
@@ -181,49 +183,98 @@ static const std::string logger_name = "caosdb::transaction";
 /**
  * Abstract base class for the results of a Transaction.
  */
-class ResultSet {
+template <class T> class ResultSet {
   class iterator;
 
 public:
   virtual ~ResultSet() = default;
   [[nodiscard]] virtual auto Size() const noexcept -> int = 0;
-  [[nodiscard]] virtual auto At(const int index) const -> const Entity & = 0;
+  [[nodiscard]] virtual auto At(const int index) const -> const T & = 0;
   auto begin() const -> iterator;
   auto end() const -> iterator;
 
 private:
-  class iterator : public std::iterator<std::output_iterator_tag, Entity> {
+  class iterator : public std::iterator<std::output_iterator_tag, T> {
   public:
-    explicit iterator(const ResultSet *result_set, int index = 0);
-    auto operator*() const -> const Entity &;
+    explicit iterator(const ResultSet<T> *result_set, int index = 0);
+    auto operator*() const -> const T &;
     auto operator++() -> iterator &;
     auto operator++(int) -> iterator;
     auto operator!=(const iterator &rhs) const -> bool;
 
   private:
     int current_index = 0;
-    const ResultSet *result_set;
+    const ResultSet<T> *result_set;
   };
 };
 
+template <class T>
+ResultSet<T>::iterator::iterator(const ResultSet *result_set_param, int index)
+  : current_index(index), result_set(result_set_param) {}
+
+template <class T> auto ResultSet<T>::iterator::operator*() const -> const T & {
+  return this->result_set->At(current_index);
+}
+
+template <class T>
+auto ResultSet<T>::iterator::operator++() -> ResultSet<T>::iterator & {
+  current_index++;
+  return *this;
+}
+
+template <class T>
+auto ResultSet<T>::iterator::operator++(int) -> ResultSet<T>::iterator {
+  iterator tmp(*this);
+  operator++();
+  return tmp;
+}
+
+template <class T>
+auto ResultSet<T>::iterator::operator!=(const iterator &rhs) const -> bool {
+  return this->current_index != rhs.current_index;
+}
+
+template <class T> auto ResultSet<T>::begin() const -> ResultSet<T>::iterator {
+  return ResultSet<T>::iterator(this, 0);
+}
+
+template <class T> auto ResultSet<T>::end() const -> ResultSet<T>::iterator {
+  return ResultSet<T>::iterator(this, Size());
+}
+
+template <class T> class IMultiResultSet : public ResultSet<T> {
+public:
+  virtual ~IMultiResultSet() = default;
+  inline explicit IMultiResultSet(std::vector<std::unique_ptr<T>> result_set)
+    : items(std::move(result_set)) {}
+  [[nodiscard]] inline auto Size() const noexcept -> int override {
+    return this->items.size();
+  }
+  [[nodiscard]] inline auto At(const int index) const -> const T & override {
+    return *(this->items.at(index));
+  }
+
+protected:
+  std::vector<std::unique_ptr<T>> items;
+};
+
+class FilesResultSet : public IMultiResultSet<FileDescriptor> {
+public:
+  ~FilesResultSet() = default;
+  explicit FilesResultSet(
+    std::vector<std::unique_ptr<FileDescriptor>> result_set);
+};
+
 /**
  * Container with results of a transaction.
  *
  * In contrast to UniqueResult, this one can also hold multiple entities or zero
  * entities.
  */
-class MultiResultSet : public ResultSet {
+class MultiResultSet : public IMultiResultSet<Entity> {
 public:
   ~MultiResultSet() = default;
   explicit MultiResultSet(std::vector<std::unique_ptr<Entity>> result_set);
-  [[nodiscard]] inline auto Size() const noexcept -> int override {
-    return this->entities.size();
-  }
-  [[nodiscard]] inline auto At(const int index) const
-    -> const Entity & override {
-    return *(this->entities.at(index));
-  }
-  std::vector<std::unique_ptr<Entity>> entities;
 };
 
 /**
@@ -232,7 +283,7 @@ public:
  * In contrast to MultiResultSet, this one guarantees to hold exactly one
  * entity.
  */
-class UniqueResult : public ResultSet {
+class UniqueResult : public ResultSet<Entity> {
 public:
   ~UniqueResult() = default;
   explicit inline UniqueResult(ProtoEntity *protoEntity)
@@ -285,6 +336,29 @@ public:
   Transaction(std::shared_ptr<EntityTransactionService::Stub> entity_service,
               std::shared_ptr<FileTransmissionService::Stub> file_service);
 
+  /**
+   * Add an entity id to this transaction for retrieval and also download the
+   * file.
+   *
+   * If the entity doesn't have a file a warning is appended.
+   *
+   * If the file cannot be downloaded due to unsufficient permissions an error
+   * is appended.
+   */
+  auto RetrieveAndDownloadFilesById(const std::string &id) noexcept
+    -> StatusCode;
+  auto DownloadFilesById(const std::string &path) noexcept -> StatusCode;
+  auto RetrieveAndDownloadFilesByQuery(const std::string &query) noexcept
+    -> StatusCode;
+  auto DownloadFilesByQuery(const std::string &query) noexcept -> StatusCode;
+
+  /**
+   * Return a ResultSet<FileDescriptor>.
+   *
+   * The file result set is empty until the transaction terminates.
+   */
+  auto GetDownloadedFiles() const noexcept -> const FilesResultSet &;
+
   /**
    * Add an entity id to this transaction for retrieval.
    *
@@ -364,12 +438,13 @@ public:
   /**
    * Return the current status of the transaction.
    */
-  [[nodiscard]] inline auto GetStatus() const -> TransactionStatus {
+  [[nodiscard]] inline auto GetStatus() const noexcept -> TransactionStatus {
     return this->status;
   }
 
-  [[nodiscard]] inline auto GetResultSet() const -> const ResultSet & {
-    const ResultSet *result_set = this->result_set.get();
+  [[nodiscard]] inline auto GetResultSet() const noexcept
+    -> const ResultSet<Entity> & {
+    const ResultSet<Entity> *result_set = this->result_set.get();
     return *result_set;
   }
 
@@ -380,7 +455,7 @@ public:
    * this transaction. In all other cases, the return value will be
    * -1.
    */
-  [[nodiscard]] inline auto GetCountResult() const -> long {
+  [[nodiscard]] inline auto GetCountResult() const noexcept -> long {
     return query_count;
   }
 
@@ -421,7 +496,7 @@ public:
 private:
   bool has_query = false;
   TransactionType transaction_type = TransactionType::NONE;
-  mutable std::unique_ptr<ResultSet> result_set;
+  mutable std::unique_ptr<ResultSet<Entity>> result_set;
   mutable TransactionStatus status = TransactionStatus::INITIAL();
   std::shared_ptr<EntityTransactionService::Stub> entity_service;
   std::shared_ptr<FileTransmissionService::Stub> file_service;
diff --git a/src/caosdb/transaction.cpp b/src/caosdb/transaction.cpp
index e6b22583f9a018faa048e688f4824dc25cd81938..e2c524343693a35c22fe3b7dee3253a80bebcfdc 100644
--- a/src/caosdb/transaction.cpp
+++ b/src/caosdb/transaction.cpp
@@ -112,38 +112,8 @@ using grpc::ClientAsyncResponseReader;
 using ProtoEntity = caosdb::entity::v1alpha1::Entity;
 using grpc::CompletionQueue;
 
-ResultSet::iterator::iterator(const ResultSet *result_set_param, int index)
-  : current_index(index), result_set(result_set_param) {}
-
-auto ResultSet::iterator::operator*() const -> const Entity & {
-  return this->result_set->At(current_index);
-}
-
-auto ResultSet::iterator::operator++() -> ResultSet::iterator & {
-  current_index++;
-  return *this;
-}
-
-auto ResultSet::iterator::operator++(int) -> ResultSet::iterator {
-  iterator tmp(*this);
-  operator++();
-  return tmp;
-}
-
-auto ResultSet::iterator::operator!=(const iterator &rhs) const -> bool {
-  return this->current_index != rhs.current_index;
-}
-
-auto ResultSet::begin() const -> ResultSet::iterator {
-  return ResultSet::iterator(this, 0);
-}
-
-auto ResultSet::end() const -> ResultSet::iterator {
-  return ResultSet::iterator(this, Size());
-}
-
 MultiResultSet::MultiResultSet(std::vector<std::unique_ptr<Entity>> result_set)
-  : entities(std::move(result_set)) {}
+  : IMultiResultSet<Entity>(std::move(result_set)) {}
 
 [[nodiscard]] auto UniqueResult::GetEntity() const -> const Entity & {
   const Entity *result = this->entity.get();
@@ -160,6 +130,9 @@ Transaction::Transaction(
   this->entity_service = std::move(entity_service);
   this->file_service = std::move(file_service);
   this->query_count = -1;
+}
+
+auto Transaction::RetrieveById(const std::string &id) noexcept -> StatusCode {
   ASSERT_CAN_ADD_RETRIEVAL
 
   auto *sub_request = this->request->add_requests();
diff --git a/test/test_transaction.cpp b/test/test_transaction.cpp
index 6e04976d7d203e29c0ba084896e2f897c6121d48..b062cb8a6372ac5db749cc9b6acd998f8829a14a 100644
--- a/test/test_transaction.cpp
+++ b/test/test_transaction.cpp
@@ -182,7 +182,7 @@ TEST(test_transaction, test_multi_deletion) {
   auto configuration = InsecureConnectionConfiguration(host, 8000);
   Connection connection(configuration);
   auto transaction = connection.CreateTransaction();
-  EXPECT_EQ(transaction.GetStatus().GetCode(), StatusCode::INITIAL);
+  EXPECT_EQ(transaction->GetStatus().GetCode(), StatusCode::INITIAL);
   for (int i = 0; i < 3; i++) {
     auto status = transaction->DeleteById("asdf");
     EXPECT_EQ(status, StatusCode::GO_ON);