diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index 5871ead42f2b9fef62c1bfa9ea6189642ea59c4f..5074a479657ac0aee77ede05636325d4c77f05d6 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -13,6 +13,7 @@
 - cta/CTA#983 Add cta-release package for public binary rpm distribution.
 - cta/CTA#980 Add external encryption script option
 - cta/CTA#976 Define a new table in the DB schema to contain the drive status.
+- cta/CTA#834 New command "recycletf restore" allows undeleting a copy of a file from tape deleted using tapefile rm
 
 - [frontend] New command "tapefile rm" allows deleting a copy of a file from tape
 
diff --git a/catalogue/Catalogue.hpp b/catalogue/Catalogue.hpp
index 0a93a611bcf64b3db66f02e3b22617b1e29c0b51..60b813fdc9674eebc07e005939983ea9b71d6b12 100644
--- a/catalogue/Catalogue.hpp
+++ b/catalogue/Catalogue.hpp
@@ -123,6 +123,7 @@ CTA_GENERATE_USER_EXCEPTION_CLASS(UserSpecifiedStorageClassUsedByArchiveFiles);
 CTA_GENERATE_USER_EXCEPTION_CLASS(UserSpecifiedStorageClassUsedByArchiveRoutes);
 CTA_GENERATE_USER_EXCEPTION_CLASS(UserSpecifiedStorageClassUsedByFileRecycleLogs);
 CTA_GENERATE_USER_EXCEPTION_CLASS(UserSpecifiedTapePoolUsedInAnArchiveRoute);
+CTA_GENERATE_USER_EXCEPTION_CLASS(UserSpecifiedExistingDeletedFileCopy);
 CTA_GENERATE_USER_EXCEPTION_CLASS(UserSpecifiedANonExistentTapeState);
 CTA_GENERATE_USER_EXCEPTION_CLASS(UserSpecifiedAnEmptyStringReasonWhenTapeStateNotActive);
 
@@ -900,6 +901,15 @@ public:
    * @return The deleted archive files ordered by archive file ID.
    */
   virtual FileRecycleLogItor getFileRecycleLogItor(const RecycleTapeFileSearchCriteria & searchCriteria = RecycleTapeFileSearchCriteria()) const = 0;
+  
+
+  /**
+   * Restores the deleted files in the Recycle log that match the criteria passed
+   *
+   * @param searchCriteria The search criteria
+   */
+  virtual void restoreFilesInRecycleLog(const RecycleTapeFileSearchCriteria & searchCriteria = RecycleTapeFileSearchCriteria()) = 0;
+
 
   /**
    * Returns the specified files in tape file sequence order.
@@ -969,6 +979,7 @@ public:
       const std::string &reason) = 0;
 
 
+
   /**
    * Returns the archive file with the specified unique identifier.
    *
diff --git a/catalogue/CatalogueRetryWrapper.hpp b/catalogue/CatalogueRetryWrapper.hpp
index 84eed51251e79a2a6754591513f9bda6d952c514..efa9e0f2bb02cff8f34bc57ae8edb404ef8e2454 100644
--- a/catalogue/CatalogueRetryWrapper.hpp
+++ b/catalogue/CatalogueRetryWrapper.hpp
@@ -533,6 +533,10 @@ public:
     return retryOnLostConnection(m_log, [&]{return m_catalogue->getFileRecycleLogItor(searchCriteria);}, m_maxTriesToConnect);
   }
   
+  void restoreFilesInRecycleLog(const RecycleTapeFileSearchCriteria & searchCriteria) override {
+    return retryOnLostConnection(m_log, [&]{return m_catalogue->restoreFilesInRecycleLog(searchCriteria);}, m_maxTriesToConnect);
+  }
+
   void deleteFileFromRecycleBin(const uint64_t archiveFileId, log::LogContext &lc){
     return retryOnLostConnection(m_log,[&]{return m_catalogue->deleteFileFromRecycleBin(archiveFileId,lc);},m_maxTriesToConnect);
   }
diff --git a/catalogue/CatalogueTest.cpp b/catalogue/CatalogueTest.cpp
index a931dfdc3eb290888f2e3be92c3be832b920210f..c21f770b83bb79adf848b84be8f5a9edbe8995e4 100644
--- a/catalogue/CatalogueTest.cpp
+++ b/catalogue/CatalogueTest.cpp
@@ -124,6 +124,17 @@ namespace {
     return storageClass;
   }
 
+  cta::common::dataStructures::StorageClass getStorageClassTripleCopy() {
+    using namespace cta;
+
+    common::dataStructures::StorageClass storageClass;
+    storageClass.name = "storage_class_triple_copy";
+    storageClass.nbCopies = 3;
+    storageClass.vo.name = getVo().name;
+    storageClass.comment = "Creation of storage class with 3 copies on tape";
+    return storageClass;
+  }
+
   cta::catalogue::MediaType getMediaType() {
     using namespace cta;
 
@@ -164,6 +175,14 @@ namespace {
     tape.comment = "Creation of tape two";
     return tape;
   }
+
+    cta::catalogue::CreateTapeAttributes getTape3() {
+    // Tape 3 is an exact copy of tape 1 except for its VID and comment
+    auto tape = getTape1();
+    tape.vid = "VIDTHREE";
+    tape.comment = "Creation of tape three";
+    return tape;
+  }
   
   cta::catalogue::CreateMountPolicyAttributes getMountPolicy1() {
     using namespace cta;
@@ -191,9 +210,11 @@ cta_catalogue_CatalogueTest::cta_catalogue_CatalogueTest():
         m_storageClassSingleCopy(getStorageClass()),
         m_anotherStorageClass(getAnotherStorageClass()),
         m_storageClassDualCopy(getStorageClassDualCopy()),
+        m_storageClassTripleCopy(getStorageClassTripleCopy()),
         m_mediaType(getMediaType()),
         m_tape1(getTape1()),
-        m_tape2(getTape2()) {
+        m_tape2(getTape2()),
+        m_tape3(getTape3()) {
 }
 
 //------------------------------------------------------------------------------
@@ -16097,4 +16118,452 @@ TEST_P(cta_catalogue_CatalogueTest, DeleteTapeFileCopyUsingFXID) {
   }
 }
 
+TEST_P(cta_catalogue_CatalogueTest, RestoreTapeFileCopy) {
+  using namespace cta;
+
+  const bool logicalLibraryIsDisabled= false;
+  const std::string tapePoolName1 = "tape_pool_name_1";
+  const std::string tapePoolName2 = "tape_pool_name_2";
+  const uint64_t nbPartialTapes = 1;
+  const bool isEncrypted = true;
+  const cta::optional<std::string> supply("value for the supply pool mechanism");
+  const std::string diskInstance = "disk_instance";
+  const std::string tapeDrive = "tape_drive";
+  const std::string reason = "reason";
+
+  m_catalogue->createMediaType(m_admin, m_mediaType);
+  m_catalogue->createLogicalLibrary(m_admin, m_tape1.logicalLibraryName, logicalLibraryIsDisabled, "Create logical library");
+  m_catalogue->createVirtualOrganization(m_admin, m_vo);
+  m_catalogue->createTapePool(m_admin, tapePoolName1, m_vo.name, nbPartialTapes, isEncrypted, supply, "Create tape pool");
+  m_catalogue->createTapePool(m_admin, tapePoolName2, m_vo.name, nbPartialTapes, isEncrypted, supply, "Create tape pool");
+  m_catalogue->createStorageClass(m_admin, m_storageClassDualCopy);
+
+  auto tape1 = m_tape1;
+  auto tape2 = m_tape2;
+  tape1.tapePoolName = tapePoolName1;
+  tape2.tapePoolName = tapePoolName2;
+
+  m_catalogue->createTape(m_admin, tape1);
+  m_catalogue->createTape(m_admin, tape2);
+
+  ASSERT_FALSE(m_catalogue->getArchiveFilesItor().hasMore());
+  const uint64_t archiveFileSize = 2 * 1000 * 1000 * 1000;
+
+
+  // Write a file on tape
+  {
+    std::set<catalogue::TapeItemWrittenPointer> tapeFilesWrittenCopy1;
+    
+    std::ostringstream diskFileId;
+    diskFileId << 12345677;
+
+    std::ostringstream diskFilePath;
+    diskFilePath << "/test/file1";
+
+    auto fileWrittenUP=cta::make_unique<cta::catalogue::TapeFileWritten>();
+    auto & fileWritten = *fileWrittenUP;
+    fileWritten.archiveFileId = 1;
+    fileWritten.diskInstance = diskInstance;
+    fileWritten.diskFileId = diskFileId.str();
+    fileWritten.diskFilePath = diskFilePath.str();
+    fileWritten.diskFileOwnerUid = PUBLIC_DISK_USER;
+    fileWritten.diskFileGid = PUBLIC_DISK_GROUP;
+    fileWritten.size = archiveFileSize;
+    fileWritten.checksumBlob.insert(checksum::ADLER32, "1357");
+    fileWritten.storageClassName = m_storageClassDualCopy.name;
+    fileWritten.vid = tape1.vid;
+    fileWritten.fSeq = 1;
+    fileWritten.blockId = 1 * 100;
+    fileWritten.copyNb = 1;
+    fileWritten.tapeDrive = tapeDrive;
+    tapeFilesWrittenCopy1.emplace(fileWrittenUP.release());
+    
+    m_catalogue->filesWrittenToTape(tapeFilesWrittenCopy1);
+  }
+  
+  // Write a second copy of file on tape
+  {
+    std::set<catalogue::TapeItemWrittenPointer> tapeFilesWrittenCopy1;
+    
+    std::ostringstream diskFileId;
+    diskFileId << 12345677;
+
+    std::ostringstream diskFilePath;
+    diskFilePath << "/test/file1";
+
+    auto fileWrittenUP=cta::make_unique<cta::catalogue::TapeFileWritten>();
+    auto & fileWritten = *fileWrittenUP;
+    fileWritten.archiveFileId = 1;
+    fileWritten.diskInstance = diskInstance;
+    fileWritten.diskFileId = diskFileId.str();
+    fileWritten.diskFilePath = diskFilePath.str();
+    fileWritten.diskFileOwnerUid = PUBLIC_DISK_USER;
+    fileWritten.diskFileGid = PUBLIC_DISK_GROUP;
+    fileWritten.size = archiveFileSize;
+    fileWritten.checksumBlob.insert(checksum::ADLER32, "1357");
+    fileWritten.storageClassName = m_storageClassDualCopy.name;
+    fileWritten.vid = tape2.vid;
+    fileWritten.fSeq = 1;
+    fileWritten.blockId = 1 * 100;
+    fileWritten.copyNb = 2;
+    fileWritten.tapeDrive = tapeDrive;
+    tapeFilesWrittenCopy1.emplace(fileWrittenUP.release());
+    
+    m_catalogue->filesWrittenToTape(tapeFilesWrittenCopy1);
+  }
+  {
+    //Assert both copies written
+    auto archiveFile = m_catalogue->getArchiveFileById(1);  
+    ASSERT_EQ(2, archiveFile.tapeFiles.size());
+  }
+
+  {
+    //delete copy of file on tape1
+    m_catalogue->deleteTapeFileCopy(tape1.vid, "BC614D", diskInstance, reason);
+    auto archiveFile = m_catalogue->getArchiveFileById(1);  
+    ASSERT_EQ(1, archiveFile.tapeFiles.size());
+  }
+
+
+  {
+    //restore copy of file on tape1
+    catalogue::RecycleTapeFileSearchCriteria searchCriteria;
+    searchCriteria.archiveFileId = 1;
+    searchCriteria.vid = tape1.vid;
+
+    m_catalogue->restoreFilesInRecycleLog(searchCriteria);
+    auto archiveFile = m_catalogue->getArchiveFileById(1); 
+    //assert both copies present
+    ASSERT_EQ(2, archiveFile.tapeFiles.size());
+
+    //assert recycle log is empty
+    auto fileRecycleLogItor = m_catalogue->getFileRecycleLogItor();
+    ASSERT_FALSE(fileRecycleLogItor.hasMore());
+    
+  }
+}
+
+TEST_P(cta_catalogue_CatalogueTest, RestoreRewrittenTapeFileCopyFails) {
+  using namespace cta;
+
+  const bool logicalLibraryIsDisabled= false;
+  const std::string tapePoolName1 = "tape_pool_name_1";
+  const std::string tapePoolName2 = "tape_pool_name_2";
+  const uint64_t nbPartialTapes = 1;
+  const bool isEncrypted = true;
+  const cta::optional<std::string> supply("value for the supply pool mechanism");
+  const std::string diskInstance = "disk_instance";
+  const std::string tapeDrive = "tape_drive";
+  const std::string reason = "reason";
+
+  m_catalogue->createMediaType(m_admin, m_mediaType);
+  m_catalogue->createLogicalLibrary(m_admin, m_tape1.logicalLibraryName, logicalLibraryIsDisabled, "Create logical library");
+  m_catalogue->createVirtualOrganization(m_admin, m_vo);
+  m_catalogue->createTapePool(m_admin, tapePoolName1, m_vo.name, nbPartialTapes, isEncrypted, supply, "Create tape pool");
+  m_catalogue->createTapePool(m_admin, tapePoolName2, m_vo.name, nbPartialTapes, isEncrypted, supply, "Create tape pool");
+  m_catalogue->createStorageClass(m_admin, m_storageClassDualCopy);
+
+  auto tape1 = m_tape1;
+  auto tape2 = m_tape2;
+  tape1.tapePoolName = tapePoolName1;
+  tape2.tapePoolName = tapePoolName2;
+
+  m_catalogue->createTape(m_admin, tape1);
+  m_catalogue->createTape(m_admin, tape2);
+
+  ASSERT_FALSE(m_catalogue->getArchiveFilesItor().hasMore());
+  const uint64_t archiveFileSize = 2 * 1000 * 1000 * 1000;
+
+
+  // Write a file on tape
+  {
+    std::set<catalogue::TapeItemWrittenPointer> tapeFilesWrittenCopy1;
+    
+    std::ostringstream diskFileId;
+    diskFileId << 12345677;
+
+    std::ostringstream diskFilePath;
+    diskFilePath << "/test/file1";
+
+    auto fileWrittenUP=cta::make_unique<cta::catalogue::TapeFileWritten>();
+    auto & fileWritten = *fileWrittenUP;
+    fileWritten.archiveFileId = 1;
+    fileWritten.diskInstance = diskInstance;
+    fileWritten.diskFileId = diskFileId.str();
+    fileWritten.diskFilePath = diskFilePath.str();
+    fileWritten.diskFileOwnerUid = PUBLIC_DISK_USER;
+    fileWritten.diskFileGid = PUBLIC_DISK_GROUP;
+    fileWritten.size = archiveFileSize;
+    fileWritten.checksumBlob.insert(checksum::ADLER32, "1357");
+    fileWritten.storageClassName = m_storageClassDualCopy.name;
+    fileWritten.vid = tape1.vid;
+    fileWritten.fSeq = 1;
+    fileWritten.blockId = 1 * 100;
+    fileWritten.copyNb = 1;
+    fileWritten.tapeDrive = tapeDrive;
+    tapeFilesWrittenCopy1.emplace(fileWrittenUP.release());
+    
+    m_catalogue->filesWrittenToTape(tapeFilesWrittenCopy1);
+  }
+  
+  // Write a second copy of file on tape
+  {
+    std::set<catalogue::TapeItemWrittenPointer> tapeFilesWrittenCopy1;
+    
+    std::ostringstream diskFileId;
+    diskFileId << 12345677;
+
+    std::ostringstream diskFilePath;
+    diskFilePath << "/test/file1";
+
+    auto fileWrittenUP=cta::make_unique<cta::catalogue::TapeFileWritten>();
+    auto & fileWritten = *fileWrittenUP;
+    fileWritten.archiveFileId = 1;
+    fileWritten.diskInstance = diskInstance;
+    fileWritten.diskFileId = diskFileId.str();
+    fileWritten.diskFilePath = diskFilePath.str();
+    fileWritten.diskFileOwnerUid = PUBLIC_DISK_USER;
+    fileWritten.diskFileGid = PUBLIC_DISK_GROUP;
+    fileWritten.size = archiveFileSize;
+    fileWritten.checksumBlob.insert(checksum::ADLER32, "1357");
+    fileWritten.storageClassName = m_storageClassDualCopy.name;
+    fileWritten.vid = tape2.vid;
+    fileWritten.fSeq = 1;
+    fileWritten.blockId = 1 * 100;
+    fileWritten.copyNb = 2;
+    fileWritten.tapeDrive = tapeDrive;
+    tapeFilesWrittenCopy1.emplace(fileWrittenUP.release());
+    
+    m_catalogue->filesWrittenToTape(tapeFilesWrittenCopy1);
+  }
+  {
+    //Assert both copies written
+    auto archiveFile = m_catalogue->getArchiveFileById(1);  
+    ASSERT_EQ(2, archiveFile.tapeFiles.size());
+  }
+
+  {
+    //delete copy of file on tape1
+    m_catalogue->deleteTapeFileCopy(tape1.vid, "BC614D", diskInstance, reason);
+    auto archiveFile = m_catalogue->getArchiveFileById(1);  
+    ASSERT_EQ(1, archiveFile.tapeFiles.size());
+  }
+
+    // Rewrite deleted copy of file on tape
+  {
+    std::set<catalogue::TapeItemWrittenPointer> tapeFilesWrittenCopy1;
+    
+    std::ostringstream diskFileId;
+    diskFileId << 12345677;
+
+    std::ostringstream diskFilePath;
+    diskFilePath << "/test/file1";
+
+    auto fileWrittenUP=cta::make_unique<cta::catalogue::TapeFileWritten>();
+    auto & fileWritten = *fileWrittenUP;
+    fileWritten.archiveFileId = 1;
+    fileWritten.diskInstance = diskInstance;
+    fileWritten.diskFileId = diskFileId.str();
+    fileWritten.diskFilePath = diskFilePath.str();
+    fileWritten.diskFileOwnerUid = PUBLIC_DISK_USER;
+    fileWritten.diskFileGid = PUBLIC_DISK_GROUP;
+    fileWritten.size = archiveFileSize;
+    fileWritten.checksumBlob.insert(checksum::ADLER32, "1357");
+    fileWritten.storageClassName = m_storageClassDualCopy.name;
+    fileWritten.vid = tape1.vid;
+    fileWritten.fSeq = 2;
+    fileWritten.blockId = 1 * 100;
+    fileWritten.copyNb = 1;
+    fileWritten.tapeDrive = tapeDrive;
+    tapeFilesWrittenCopy1.emplace(fileWrittenUP.release());
+    
+    m_catalogue->filesWrittenToTape(tapeFilesWrittenCopy1);
+  }
+
+  {
+    //restore copy of file on tape1
+    catalogue::RecycleTapeFileSearchCriteria searchCriteria;
+    searchCriteria.archiveFileId = 1;
+    searchCriteria.vid = tape1.vid;
+
+    ASSERT_THROW(m_catalogue->restoreFilesInRecycleLog(searchCriteria), catalogue::UserSpecifiedExistingDeletedFileCopy);
+    auto archiveFile = m_catalogue->getArchiveFileById(1); 
+    //assert only two copies present
+    ASSERT_EQ(2, archiveFile.tapeFiles.size());
+
+    //assert recycle log still contains deleted copy
+    auto fileRecycleLogItor = m_catalogue->getFileRecycleLogItor();
+    ASSERT_TRUE(fileRecycleLogItor.hasMore());
+    
+  }
+}
+
+TEST_P(cta_catalogue_CatalogueTest, RestoreVariousDeletedTapeFileCopies) {
+  using namespace cta;
+
+  const bool logicalLibraryIsDisabled= false;
+  const std::string tapePoolName1 = "tape_pool_name_1";
+  const std::string tapePoolName2 = "tape_pool_name_2";
+  const std::string tapePoolName3 = "tape_pool_name_3";
+  const uint64_t nbPartialTapes = 1;
+  const bool isEncrypted = true;
+  const cta::optional<std::string> supply("value for the supply pool mechanism");
+  const std::string diskInstance = "disk_instance";
+  const std::string tapeDrive = "tape_drive";
+  const std::string reason = "reason";
+
+  m_catalogue->createMediaType(m_admin, m_mediaType);
+  m_catalogue->createLogicalLibrary(m_admin, m_tape1.logicalLibraryName, logicalLibraryIsDisabled, "Create logical library");
+  m_catalogue->createVirtualOrganization(m_admin, m_vo);
+  m_catalogue->createTapePool(m_admin, tapePoolName1, m_vo.name, nbPartialTapes, isEncrypted, supply, "Create tape pool");
+  m_catalogue->createTapePool(m_admin, tapePoolName2, m_vo.name, nbPartialTapes, isEncrypted, supply, "Create tape pool");
+  m_catalogue->createTapePool(m_admin, tapePoolName3, m_vo.name, nbPartialTapes, isEncrypted, supply, "Create tape pool");
+  m_catalogue->createStorageClass(m_admin, m_storageClassTripleCopy);
+
+  auto tape1 = m_tape1;
+  auto tape2 = m_tape2;
+  auto tape3 = m_tape3;
+  tape1.tapePoolName = tapePoolName1;
+  tape2.tapePoolName = tapePoolName2;
+  tape3.tapePoolName = tapePoolName3;
+
+  m_catalogue->createTape(m_admin, tape1);
+  m_catalogue->createTape(m_admin, tape2);
+  m_catalogue->createTape(m_admin, tape3);
+
+  ASSERT_FALSE(m_catalogue->getArchiveFilesItor().hasMore());
+  const uint64_t archiveFileSize = 2 * 1000 * 1000 * 1000;
+
+
+  // Write a file on tape
+  {
+    std::set<catalogue::TapeItemWrittenPointer> tapeFilesWrittenCopy1;
+    
+    std::ostringstream diskFileId;
+    diskFileId << 12345677;
+
+    std::ostringstream diskFilePath;
+    diskFilePath << "/test/file1";
+
+    auto fileWrittenUP=cta::make_unique<cta::catalogue::TapeFileWritten>();
+    auto & fileWritten = *fileWrittenUP;
+    fileWritten.archiveFileId = 1;
+    fileWritten.diskInstance = diskInstance;
+    fileWritten.diskFileId = diskFileId.str();
+    fileWritten.diskFilePath = diskFilePath.str();
+    fileWritten.diskFileOwnerUid = PUBLIC_DISK_USER;
+    fileWritten.diskFileGid = PUBLIC_DISK_GROUP;
+    fileWritten.size = archiveFileSize;
+    fileWritten.checksumBlob.insert(checksum::ADLER32, "1357");
+    fileWritten.storageClassName = m_storageClassTripleCopy.name;
+    fileWritten.vid = tape1.vid;
+    fileWritten.fSeq = 1;
+    fileWritten.blockId = 1 * 100;
+    fileWritten.copyNb = 1;
+    fileWritten.tapeDrive = tapeDrive;
+    tapeFilesWrittenCopy1.emplace(fileWrittenUP.release());
+    
+    m_catalogue->filesWrittenToTape(tapeFilesWrittenCopy1);
+  }
+  
+  // Write a second copy of file on tape
+  {
+    std::set<catalogue::TapeItemWrittenPointer> tapeFilesWrittenCopy1;
+    
+    std::ostringstream diskFileId;
+    diskFileId << 12345677;
+
+    std::ostringstream diskFilePath;
+    diskFilePath << "/test/file1";
+
+    auto fileWrittenUP=cta::make_unique<cta::catalogue::TapeFileWritten>();
+    auto & fileWritten = *fileWrittenUP;
+    fileWritten.archiveFileId = 1;
+    fileWritten.diskInstance = diskInstance;
+    fileWritten.diskFileId = diskFileId.str();
+    fileWritten.diskFilePath = diskFilePath.str();
+    fileWritten.diskFileOwnerUid = PUBLIC_DISK_USER;
+    fileWritten.diskFileGid = PUBLIC_DISK_GROUP;
+    fileWritten.size = archiveFileSize;
+    fileWritten.checksumBlob.insert(checksum::ADLER32, "1357");
+    fileWritten.storageClassName = m_storageClassTripleCopy.name;
+    fileWritten.vid = tape2.vid;
+    fileWritten.fSeq = 1;
+    fileWritten.blockId = 1 * 100;
+    fileWritten.copyNb = 2;
+    fileWritten.tapeDrive = tapeDrive;
+    tapeFilesWrittenCopy1.emplace(fileWrittenUP.release());
+    
+    m_catalogue->filesWrittenToTape(tapeFilesWrittenCopy1);
+  }
+
+  // Write a third copy of file on tape
+  {
+    std::set<catalogue::TapeItemWrittenPointer> tapeFilesWrittenCopy1;
+    
+    std::ostringstream diskFileId;
+    diskFileId << 12345677;
+
+    std::ostringstream diskFilePath;
+    diskFilePath << "/test/file1";
+
+    auto fileWrittenUP=cta::make_unique<cta::catalogue::TapeFileWritten>();
+    auto & fileWritten = *fileWrittenUP;
+    fileWritten.archiveFileId = 1;
+    fileWritten.diskInstance = diskInstance;
+    fileWritten.diskFileId = diskFileId.str();
+    fileWritten.diskFilePath = diskFilePath.str();
+    fileWritten.diskFileOwnerUid = PUBLIC_DISK_USER;
+    fileWritten.diskFileGid = PUBLIC_DISK_GROUP;
+    fileWritten.size = archiveFileSize;
+    fileWritten.checksumBlob.insert(checksum::ADLER32, "1357");
+    fileWritten.storageClassName = m_storageClassTripleCopy.name;
+    fileWritten.vid = tape3.vid;
+    fileWritten.fSeq = 1;
+    fileWritten.blockId = 1 * 100;
+    fileWritten.copyNb = 3;
+    fileWritten.tapeDrive = tapeDrive;
+    tapeFilesWrittenCopy1.emplace(fileWrittenUP.release());
+    
+    m_catalogue->filesWrittenToTape(tapeFilesWrittenCopy1);
+  }
+  {
+    //Assert all copies written
+    auto archiveFile = m_catalogue->getArchiveFileById(1);  
+    ASSERT_EQ(3, archiveFile.tapeFiles.size());
+  }
+
+  {
+    //delete copy of file on tape1
+    m_catalogue->deleteTapeFileCopy(tape1.vid, "BC614D", diskInstance, reason);
+    auto archiveFile = m_catalogue->getArchiveFileById(1);  
+    ASSERT_EQ(2, archiveFile.tapeFiles.size());
+  }
+
+  {
+    //delete copy of file on tape2
+    m_catalogue->deleteTapeFileCopy(tape2.vid, "BC614D", diskInstance, reason);
+    auto archiveFile = m_catalogue->getArchiveFileById(1);  
+    ASSERT_EQ(1, archiveFile.tapeFiles.size());
+  }
+
+
+  {
+    //restore all deleted copies
+    catalogue::RecycleTapeFileSearchCriteria searchCriteria;
+    searchCriteria.archiveFileId = 1;
+
+    m_catalogue->restoreFilesInRecycleLog(searchCriteria);
+    auto archiveFile = m_catalogue->getArchiveFileById(1); 
+    //assert only two copies present
+    ASSERT_EQ(3, archiveFile.tapeFiles.size());
+
+    //assert recycle log still contains deleted copy
+    auto fileRecycleLogItor = m_catalogue->getFileRecycleLogItor();
+    ASSERT_FALSE(fileRecycleLogItor.hasMore());
+    
+  }
+}
+
+
 } // namespace unitTests
diff --git a/catalogue/CatalogueTest.hpp b/catalogue/CatalogueTest.hpp
index b15bb24f6b9e6a3f3c22bec44c2997921388acfa..d556ad63aa0688e09d1bec7bff67fb21d4fc6ab2 100644
--- a/catalogue/CatalogueTest.hpp
+++ b/catalogue/CatalogueTest.hpp
@@ -46,9 +46,11 @@ protected:
   const cta::common::dataStructures::StorageClass m_storageClassSingleCopy;
   const cta::common::dataStructures::StorageClass m_anotherStorageClass;
   const cta::common::dataStructures::StorageClass m_storageClassDualCopy;
+  const cta::common::dataStructures::StorageClass m_storageClassTripleCopy;
   const cta::catalogue::MediaType m_mediaType;
   const cta::catalogue::CreateTapeAttributes m_tape1;
   const cta::catalogue::CreateTapeAttributes m_tape2;
+  const cta::catalogue::CreateTapeAttributes m_tape3;
 
   virtual void SetUp();
 
diff --git a/catalogue/DummyCatalogue.hpp b/catalogue/DummyCatalogue.hpp
index 62e5deed52b9b3e050f676c6b394fb674ac15c8b..a0082a00c05f3eca5d9fbb4cd4cf7fb43db53865 100644
--- a/catalogue/DummyCatalogue.hpp
+++ b/catalogue/DummyCatalogue.hpp
@@ -82,6 +82,7 @@ public:
   common::dataStructures::ArchiveFile getArchiveFileById(const uint64_t id) const override { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
   ArchiveFileItor getArchiveFilesItor(const TapeFileSearchCriteria& searchCriteria) const { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
   FileRecycleLogItor getFileRecycleLogItor(const RecycleTapeFileSearchCriteria & searchCriteria) const { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
+  void restoreFilesInRecycleLog(const RecycleTapeFileSearchCriteria & searchCriteria) { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
   void deleteFileFromRecycleBin(const uint64_t archiveFileId, log::LogContext &lc) {throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented");}
   void deleteFilesFromRecycleLog(const std::string & vid, log::LogContext & lc) {throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented");}
   std::list<common::dataStructures::ArchiveFile> getFilesForRepack(const std::string &vid, const uint64_t startFSeq, const uint64_t maxNbFiles) const override { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
diff --git a/catalogue/MysqlCatalogue.cpp b/catalogue/MysqlCatalogue.cpp
index f9c184f415ac7979b9c60bae07289dbd074117c3..f8f450b0064fdf6a635e4bc723e48e32fd8c6b1d 100644
--- a/catalogue/MysqlCatalogue.cpp
+++ b/catalogue/MysqlCatalogue.cpp
@@ -820,5 +820,95 @@ void MysqlCatalogue::copyTapeFileToFileRecyleLogAndDelete(rdbms::Conn & conn, co
   } 
 }
 
+//------------------------------------------------------------------------------
+// restoreFileCopiesInRecycleLog
+//------------------------------------------------------------------------------
+void MysqlCatalogue::restoreFileCopiesInRecycleLog(rdbms::Conn & conn, FileRecycleLogItor &fileRecycleLogItor, log::LogContext & lc) {
+try {
+    utils::Timer t;
+    log::TimingList tl;
+
+    //put fileRecycleLogs in std::list so we can release the underlying fileRecycleLogItor database connection
+    //otherwise we are using two conns when calling getArchiveFilesItor
+    std::list<common::dataStructures::FileRecycleLog> fileRecycleLogList;
+    while (fileRecycleLogItor.hasMore()) {
+      auto fileRecycleLog = fileRecycleLogItor.next();
+      fileRecycleLogList.push_back(fileRecycleLog);
+    }
+
+    //We currently do all file copies restoring in a single transaction
+    conn.executeNonQuery("START TRANSACTION");
+    for (auto &fileRecycleLog: fileRecycleLogList) {     
+      TapeFileSearchCriteria searchCriteria;
+      searchCriteria.archiveFileId = fileRecycleLog.archiveFileId;
+      searchCriteria.diskInstance = fileRecycleLog.diskInstanceName;
+      searchCriteria.diskFileIds = std::vector<std::string>();
+      searchCriteria.diskFileIds.value().push_back(fileRecycleLog.diskFileId);
+
+      auto itor = getArchiveFilesItor(conn, searchCriteria); 
+      if (itor.hasMore()) {
+        //only restore file copies, do nothing if file has been completely deleted in CTA
+        cta::common::dataStructures::ArchiveFile archiveFile = itor.next();
+        if (archiveFile.tapeFiles.find(fileRecycleLog.copyNb) != archiveFile.tapeFiles.end()) {
+          //copy with same copy_nb exists, cannot restore
+          UserSpecifiedExistingDeletedFileCopy ex;
+          ex.getMessage() << "Cannot restore file copy with archiveFileId " << std::to_string(fileRecycleLog.archiveFileId) 
+          << " and copy_nb " << std::to_string(fileRecycleLog.copyNb) << " because a tapefile with same archiveFileId and copy_nb already exists";
+          throw ex;
+        }
+        restoreFileCopyInRecycleLog(conn, fileRecycleLog, lc);
+      }
+    }
+    conn.commit();
+
+    log::ScopedParamContainer spc(lc);
+    tl.insertAndReset("commitTime",t);
+    tl.addToLog(spc);
+    lc.log(log::INFO,"In MysqlCatalogue::restoreFileCopiesInRecycleLog: all file copies successfully restored.");
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
+//------------------------------------------------------------------------------
+// restoreFileCopyInRecycleLog
+//------------------------------------------------------------------------------
+void MysqlCatalogue::restoreFileCopyInRecycleLog(rdbms::Conn & conn, const common::dataStructures::FileRecycleLog &fileRecycleLog, log::LogContext & lc) {
+  try {
+    utils::Timer t;
+    log::TimingList tl;
+    cta::common::dataStructures::TapeFile tapeFile;
+    tapeFile.vid = fileRecycleLog.vid;
+    tapeFile.fSeq = fileRecycleLog.fSeq;
+    tapeFile.copyNb = fileRecycleLog.copyNb;
+    tapeFile.blockId = fileRecycleLog.blockId;
+    tapeFile.fileSize = fileRecycleLog.sizeInBytes;
+    tapeFile.creationTime = fileRecycleLog.tapeFileCreationTime;
+
+    insertTapeFile(conn, tapeFile, fileRecycleLog.archiveFileId);
+    tl.insertAndReset("insertTapeFileTime",t);
+    
+    deleteTapeFileCopyFromRecycleBin(conn, fileRecycleLog);
+    tl.insertAndReset("deleteTapeFileCopyFromRecycleBinTime",t);
+    
+    log::ScopedParamContainer spc(lc);
+    spc.add("vid", tapeFile.vid);
+    spc.add("archiveFileId", fileRecycleLog.archiveFileId);
+    spc.add("fSeq", tapeFile.fSeq);
+    spc.add("copyNb", tapeFile.copyNb);
+    spc.add("fileSize", tapeFile.fileSize);
+    tl.addToLog(spc);
+    lc.log(log::INFO,"In MysqlCatalogue::restoreFileCopyInRecycleLog: File restored from the recycle log.");
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
 } // namespace catalogue
 } // namespace cta
diff --git a/catalogue/MysqlCatalogue.hpp b/catalogue/MysqlCatalogue.hpp
index 74ed0b0f5efd921bb7e425d520b7ea0f8af0ab55..75df118fd39fbd9d4da9adf8d5f62617771f7a5b 100644
--- a/catalogue/MysqlCatalogue.hpp
+++ b/catalogue/MysqlCatalogue.hpp
@@ -219,6 +219,22 @@ protected:
   void copyTapeFileToFileRecyleLogAndDelete(rdbms::Conn & conn, const cta::common::dataStructures::ArchiveFile &file, 
                                             const std::string &reason, log::LogContext & lc) override;
 
+  /**
+   * Copy the files in fileRecycleLogItor to the TAPE_FILE table and deletes the corresponding FILE_RECYCLE_LOG table entries
+   * @param conn the database connection
+   * @param fileRecycleLogItor the collection of fileRecycleLogs we want to restore
+   * @param lc the log context
+   */
+  void restoreFileCopiesInRecycleLog(rdbms::Conn & conn, FileRecycleLogItor &fileRecycleLogItor, log::LogContext & lc) override;
+
+  /**
+   * Copy the fileRecycleLog to the TAPE_FILE table and deletes the corresponding FILE_RECYCLE_LOG table entry
+   * @param conn the database connection
+   * @param fileRecycleLog the fileRecycleLog we want to restore
+   * @param lc the log context
+   */
+  void restoreFileCopyInRecycleLog(rdbms::Conn & conn, const common::dataStructures::FileRecycleLog &fileRecycleLogItor, log::LogContext & lc);
+
 
 private:
 
diff --git a/catalogue/OracleCatalogue.cpp b/catalogue/OracleCatalogue.cpp
index e15b23d3d0f994cb26bc57a0c5f3364ddd859e56..98717eee30bc6ff9500e5670629ec3c5d6cd9c52 100644
--- a/catalogue/OracleCatalogue.cpp
+++ b/catalogue/OracleCatalogue.cpp
@@ -1149,6 +1149,99 @@ void OracleCatalogue::copyTapeFileToFileRecyleLogAndDelete(rdbms::Conn & conn, c
   } 
 }
 
+//------------------------------------------------------------------------------
+// restoreFileCopiesInRecycleLog
+//------------------------------------------------------------------------------
+void OracleCatalogue::restoreFileCopiesInRecycleLog(rdbms::Conn & conn, FileRecycleLogItor &fileRecycleLogItor, log::LogContext & lc) {
+try {
+    utils::Timer t;
+    log::TimingList tl;
+
+    //put fileRecycleLogs in std::list so we can release the underlying fileRecycleLogItor database connection
+    //otherwise we are using two conns when calling getArchiveFilesItor
+    std::list<common::dataStructures::FileRecycleLog> fileRecycleLogList;
+    while (fileRecycleLogItor.hasMore()) {
+      auto fileRecycleLog = fileRecycleLogItor.next();
+      fileRecycleLogList.push_back(fileRecycleLog);
+    }
+
+    //We currently do all file copies restoring in a single transaction
+    conn.setAutocommitMode(rdbms::AutocommitMode::AUTOCOMMIT_OFF);
+    for (auto &fileRecycleLog: fileRecycleLogList) {     
+      TapeFileSearchCriteria searchCriteria;
+      searchCriteria.archiveFileId = fileRecycleLog.archiveFileId;
+      searchCriteria.diskInstance = fileRecycleLog.diskInstanceName;
+      searchCriteria.diskFileIds = std::vector<std::string>();
+      searchCriteria.diskFileIds.value().push_back(fileRecycleLog.diskFileId);
+
+      auto itor = getArchiveFilesItor(conn, searchCriteria); 
+      if (itor.hasMore()) {
+        //only restore file copies, do nothing if file has been completely deleted in CTA
+        cta::common::dataStructures::ArchiveFile archiveFile = itor.next();
+        if (archiveFile.tapeFiles.find(fileRecycleLog.copyNb) != archiveFile.tapeFiles.end()) {
+          //copy with same copy_nb exists, cannot restore
+          UserSpecifiedExistingDeletedFileCopy ex;
+          ex.getMessage() << "Cannot restore file copy with archiveFileId " << std::to_string(fileRecycleLog.archiveFileId) 
+          << " and copy_nb " << std::to_string(fileRecycleLog.copyNb) << " because a tapefile with same archiveFileId and copy_nb already exists";
+          throw ex;
+        }
+        restoreFileCopyInRecycleLog(conn, fileRecycleLog, lc);
+      }
+    }
+    conn.setAutocommitMode(rdbms::AutocommitMode::AUTOCOMMIT_ON);
+    conn.commit();
+
+    log::ScopedParamContainer spc(lc);
+    tl.insertAndReset("commitTime",t);
+    tl.addToLog(spc);
+    lc.log(log::INFO,"In OracleCatalogue::restoreFileCopiesInRecycleLog: all file copies successfully restored.");
+
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
+//------------------------------------------------------------------------------
+// restoreFileCopyInRecycleLog
+//------------------------------------------------------------------------------
+void OracleCatalogue::restoreFileCopyInRecycleLog(rdbms::Conn & conn, const common::dataStructures::FileRecycleLog &fileRecycleLog, log::LogContext & lc) {
+  try {
+    utils::Timer t;
+    log::TimingList tl;
+    cta::common::dataStructures::TapeFile tapeFile;
+    tapeFile.vid = fileRecycleLog.vid;
+    tapeFile.fSeq = fileRecycleLog.fSeq;
+    tapeFile.copyNb = fileRecycleLog.copyNb;
+    tapeFile.blockId = fileRecycleLog.blockId;
+    tapeFile.fileSize = fileRecycleLog.sizeInBytes;
+    tapeFile.creationTime = fileRecycleLog.tapeFileCreationTime;
+
+    insertTapeFile(conn, tapeFile, fileRecycleLog.archiveFileId);
+    tl.insertAndReset("insertTapeFileTime",t);
+    
+    deleteTapeFileCopyFromRecycleBin(conn, fileRecycleLog);
+    tl.insertAndReset("deleteTapeFileCopyFromRecycleBinTime",t);
+    
+    log::ScopedParamContainer spc(lc);
+    spc.add("vid", tapeFile.vid);
+    spc.add("archiveFileId", fileRecycleLog.archiveFileId);
+    spc.add("fSeq", tapeFile.fSeq);
+    spc.add("copyNb", tapeFile.copyNb);
+    spc.add("fileSize", tapeFile.fileSize);
+    tl.addToLog(spc);
+    lc.log(log::INFO,"In OracleCatalogue::restoreFileCopyInRecycleLog: File restored from the recycle log.");
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
+
 
 } // namespace catalogue
 } // namespace cta
diff --git a/catalogue/OracleCatalogue.hpp b/catalogue/OracleCatalogue.hpp
index f0c05cf0176a027691eb1eb17f90958f59a36719..172cb55e0fe1f8502355ec607ce271689a236162 100644
--- a/catalogue/OracleCatalogue.hpp
+++ b/catalogue/OracleCatalogue.hpp
@@ -262,6 +262,22 @@ private:
   void copyTapeFileToFileRecyleLogAndDelete(rdbms::Conn & conn, const cta::common::dataStructures::ArchiveFile &file, 
                                             const std::string &reason, log::LogContext & lc) override;
 
+  /**
+   * Copy the files in fileRecycleLogItor to the TAPE_FILE table and deletes the corresponding FILE_RECYCLE_LOG table entries
+   * @param conn the database connection
+   * @param fileRecycleLogItor the collection of fileRecycleLogs we want to restore
+   * @param lc the log context
+   */
+  void restoreFileCopiesInRecycleLog(rdbms::Conn & conn, FileRecycleLogItor &fileRecycleLogItor, log::LogContext & lc) override;
+
+  /**
+   * Copy the fileRecycleLog to the TAPE_FILE table and deletes the corresponding FILE_RECYCLE_LOG table entry
+   * @param conn the database connection
+   * @param fileRecycleLog the fileRecycleLog we want to restore
+   * @param lc the log context
+   */
+  void restoreFileCopyInRecycleLog(rdbms::Conn & conn, const common::dataStructures::FileRecycleLog &fileRecycleLogItor, log::LogContext & lc);
+
   /**
    * The size and checksum of a file.
    */
diff --git a/catalogue/PostgresCatalogue.cpp b/catalogue/PostgresCatalogue.cpp
index 8967dd6901303673dbb3df209f735744e46e8534..7b06ba81b33c23e5aa601ed3575d3dc467888885 100644
--- a/catalogue/PostgresCatalogue.cpp
+++ b/catalogue/PostgresCatalogue.cpp
@@ -1109,5 +1109,96 @@ void PostgresCatalogue::copyTapeFileToFileRecyleLogAndDelete(rdbms::Conn & conn,
   } 
 }
 
+//------------------------------------------------------------------------------
+// restoreFileCopiesInRecycleLog
+//------------------------------------------------------------------------------
+void PostgresCatalogue::restoreFileCopiesInRecycleLog(rdbms::Conn & conn, FileRecycleLogItor &fileRecycleLogItor, log::LogContext & lc) {
+try {
+    utils::Timer t;
+    log::TimingList tl;
+    
+    //put fileRecycleLogs in std::list so we can release the underlying fileRecycleLogItor database connection
+    //otherwise we are using two conns when calling getArchiveFilesItor
+    std::list<common::dataStructures::FileRecycleLog> fileRecycleLogList;
+    while (fileRecycleLogItor.hasMore()) {
+      auto fileRecycleLog = fileRecycleLogItor.next();
+      fileRecycleLogList.push_back(fileRecycleLog);
+    }
+
+    //We currently do all file copies restoring in a single transaction
+    conn.executeNonQuery("BEGIN");
+    for (auto &fileRecycleLog: fileRecycleLogList) {     
+      TapeFileSearchCriteria searchCriteria;
+      searchCriteria.archiveFileId = fileRecycleLog.archiveFileId;
+      searchCriteria.diskInstance = fileRecycleLog.diskInstanceName;
+      searchCriteria.diskFileIds = std::vector<std::string>();
+      searchCriteria.diskFileIds.value().push_back(fileRecycleLog.diskFileId);
+
+      auto itor = getArchiveFilesItor(conn, searchCriteria); 
+      if (itor.hasMore()) {
+        //only restore file copies, do nothing if file has been completely deleted in CTA
+        cta::common::dataStructures::ArchiveFile archiveFile = itor.next();
+        if (archiveFile.tapeFiles.find(fileRecycleLog.copyNb) != archiveFile.tapeFiles.end()) {
+          //copy with same copy_nb exists, cannot restore
+          UserSpecifiedExistingDeletedFileCopy ex;
+          ex.getMessage() << "Cannot restore file copy with archiveFileId " << std::to_string(fileRecycleLog.archiveFileId) 
+          << " and copy_nb " << std::to_string(fileRecycleLog.copyNb) << " because a tapefile with same archiveFileId and copy_nb already exists";
+          throw ex;
+        }
+        restoreFileCopyInRecycleLog(conn, fileRecycleLog, lc);
+      }
+    }
+    conn.commit();
+
+    log::ScopedParamContainer spc(lc);
+    tl.insertAndReset("commitTime",t);
+    tl.addToLog(spc);
+    lc.log(log::INFO,"In PostgresCatalogue::restoreFileCopiesInRecycleLog: all file copies successfully restored.");
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
+//------------------------------------------------------------------------------
+// restoreFileCopyInRecycleLog
+//------------------------------------------------------------------------------
+void PostgresCatalogue::restoreFileCopyInRecycleLog(rdbms::Conn & conn, const common::dataStructures::FileRecycleLog &fileRecycleLog, log::LogContext & lc) {
+  try {
+    utils::Timer t;
+    log::TimingList tl;
+    cta::common::dataStructures::TapeFile tapeFile;
+    tapeFile.vid = fileRecycleLog.vid;
+    tapeFile.fSeq = fileRecycleLog.fSeq;
+    tapeFile.copyNb = fileRecycleLog.copyNb;
+    tapeFile.blockId = fileRecycleLog.blockId;
+    tapeFile.fileSize = fileRecycleLog.sizeInBytes;
+    tapeFile.creationTime = fileRecycleLog.tapeFileCreationTime;
+
+    insertTapeFile(conn, tapeFile, fileRecycleLog.archiveFileId);
+    tl.insertAndReset("insertTapeFileTime",t);
+    
+    deleteTapeFileCopyFromRecycleBin(conn, fileRecycleLog);
+    tl.insertAndReset("deleteTapeFileCopyFromRecycleBinTime",t);
+    
+    log::ScopedParamContainer spc(lc);
+    spc.add("vid", tapeFile.vid);
+    spc.add("archiveFileId", fileRecycleLog.archiveFileId);
+    spc.add("fSeq", tapeFile.fSeq);
+    spc.add("copyNb", tapeFile.copyNb);
+    spc.add("fileSize", tapeFile.fileSize);
+    tl.addToLog(spc);
+    lc.log(log::INFO,"In PostgresCatalogue::restoreFileCopyInRecycleLog: File restored from the recycle log.");
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
+
 } // namespace catalogue
 } // namespace cta
diff --git a/catalogue/PostgresCatalogue.hpp b/catalogue/PostgresCatalogue.hpp
index 90bacb1e28ad5833af7c823b934e3423ed2a1cfe..4d50cef0d35d6fcc8062de55f08719d7b0c11ed2 100644
--- a/catalogue/PostgresCatalogue.hpp
+++ b/catalogue/PostgresCatalogue.hpp
@@ -296,6 +296,22 @@ private:
   void copyTapeFileToFileRecyleLogAndDelete(rdbms::Conn & conn, const cta::common::dataStructures::ArchiveFile &file, 
                                             const std::string &reason, log::LogContext & lc) override;
 
+  /**
+   * Copy the files in fileRecycleLogItor to the TAPE_FILE table and deletes the corresponding FILE_RECYCLE_LOG table entries
+   * @param conn the database connection
+   * @param fileRecycleLogItor the collection of fileRecycleLogs we want to restore
+   * @param lc the log context
+   */
+  void restoreFileCopiesInRecycleLog(rdbms::Conn & conn, FileRecycleLogItor &fileRecycleLogItor, log::LogContext & lc) override;
+
+  /**
+   * Copy the fileRecycleLog to the TAPE_FILE table and deletes the corresponding FILE_RECYCLE_LOG table entry
+   * @param conn the database connection
+   * @param fileRecycleLog the fileRecycleLog we want to restore
+   * @param lc the log context
+   */
+  void restoreFileCopyInRecycleLog(rdbms::Conn & conn, const common::dataStructures::FileRecycleLog &fileRecycleLogItor, log::LogContext & lc);
+
 }; // class PostgresCatalogue
 
 } // namespace catalogue
diff --git a/catalogue/RdbmsCatalogue.cpp b/catalogue/RdbmsCatalogue.cpp
index a6cd1623c2590cdc665195092c236f34e68fa3f1..0b5e51eb4b69baff50e5d121868bca1b858e315b 100644
--- a/catalogue/RdbmsCatalogue.cpp
+++ b/catalogue/RdbmsCatalogue.cpp
@@ -6936,7 +6936,14 @@ void RdbmsCatalogue::insertArchiveFile(rdbms::Conn &conn, const ArchiveFileRowWi
 //------------------------------------------------------------------------------
 void RdbmsCatalogue::checkTapeFileSearchCriteria(const TapeFileSearchCriteria &searchCriteria) const {
   auto conn = m_connPool.getConn();
+  checkTapeFileSearchCriteria(conn, searchCriteria);
 
+}
+
+//------------------------------------------------------------------------------
+// checkTapeFileSearchCriteria
+//------------------------------------------------------------------------------
+void RdbmsCatalogue::checkTapeFileSearchCriteria(rdbms::Conn &conn, const TapeFileSearchCriteria &searchCriteria) const {
   if(searchCriteria.archiveFileId) {
     if(!archiveFileIdExists(conn, searchCriteria.archiveFileId.value())) {
       throw exception::UserError(std::string("Archive file with ID ") +
@@ -6983,6 +6990,33 @@ Catalogue::ArchiveFileItor RdbmsCatalogue::getArchiveFilesItor(const TapeFileSea
   }
 }
 
+//------------------------------------------------------------------------------
+// getArchiveFilesItor
+//------------------------------------------------------------------------------
+Catalogue::ArchiveFileItor RdbmsCatalogue::getArchiveFilesItor(rdbms::Conn &conn, const TapeFileSearchCriteria &searchCriteria) const {
+
+  checkTapeFileSearchCriteria(conn, searchCriteria);
+
+  // If this is the listing of the contents of a tape
+  if (!searchCriteria.archiveFileId && !searchCriteria.diskInstance && !searchCriteria.diskFileIds &&
+    searchCriteria.vid) {
+    return getTapeContentsItor(searchCriteria.vid.value());
+  }
+
+  try {
+    auto archiveListingConn = m_archiveFileListingConnPool.getConn();
+    const auto tempDiskFxidsTableName = createAndPopulateTempTableFxid(archiveListingConn, searchCriteria.diskFileIds);
+    // Pass ownership of the connection to the Iterator object
+    auto impl = new RdbmsCatalogueGetArchiveFilesItor(m_log, std::move(archiveListingConn), searchCriteria, tempDiskFxidsTableName);
+    return ArchiveFileItor(impl);
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
 //------------------------------------------------------------------------------
 // getTapeContentsItor
 //------------------------------------------------------------------------------
@@ -7026,6 +7060,24 @@ Catalogue::FileRecycleLogItor RdbmsCatalogue::getFileRecycleLogItor(const Recycl
   }
 }
 
+
+//------------------------------------------------------------------------------
+// restoreFilesInRecycleLog
+//------------------------------------------------------------------------------
+void RdbmsCatalogue::restoreFilesInRecycleLog(const RecycleTapeFileSearchCriteria & searchCriteria) {
+  try {
+    auto fileRecycleLogitor = getFileRecycleLogItor(searchCriteria);
+    auto conn = m_connPool.getConn();
+    log::LogContext lc(m_log);  
+    restoreFileCopiesInRecycleLog(conn, fileRecycleLogitor, lc);
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
 //------------------------------------------------------------------------------
 // getFilesForRepack
 //------------------------------------------------------------------------------
@@ -9117,6 +9169,34 @@ void RdbmsCatalogue::deleteArchiveFileFromRecycleBin(rdbms::Conn& conn, const ui
   }
 }
 
+//------------------------------------------------------------------------------
+// deleteTapeFileCopyFromRecycleBin
+//------------------------------------------------------------------------------
+void RdbmsCatalogue::deleteTapeFileCopyFromRecycleBin(cta::rdbms::Conn & conn, const common::dataStructures::FileRecycleLog fileRecycleLog) {
+  try {
+    const char *const deleteTapeFilesSql = 
+    "DELETE FROM "
+      "FILE_RECYCLE_LOG "
+    "WHERE FILE_RECYCLE_LOG.ARCHIVE_FILE_ID = :ARCHIVE_FILE_ID AND FILE_RECYCLE_LOG.VID = :VID AND "
+    "FILE_RECYCLE_LOG.FSEQ = :FSEQ AND FILE_RECYCLE_LOG.COPY_NB = :COPY_NB AND "
+    "FILE_RECYCLE_LOG.DISK_INSTANCE_NAME = :DISK_INSTANCE_NAME";
+    
+    auto deleteTapeFilesStmt = conn.createStmt(deleteTapeFilesSql);
+    deleteTapeFilesStmt.bindUint64(":ARCHIVE_FILE_ID", fileRecycleLog.archiveFileId);
+    deleteTapeFilesStmt.bindString(":VID", fileRecycleLog.vid);
+    deleteTapeFilesStmt.bindUint64(":FSEQ", fileRecycleLog.fSeq);
+    deleteTapeFilesStmt.bindUint64(":COPY_NB", fileRecycleLog.copyNb);
+    deleteTapeFilesStmt.bindString(":DISK_INSTANCE_NAME", fileRecycleLog.diskInstanceName);
+    deleteTapeFilesStmt.executeNonQuery();
+    
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
 //------------------------------------------------------------------------------
 // insertOldCopiesOfFilesIfAnyOnFileRecycleLog
 //------------------------------------------------------------------------------
diff --git a/catalogue/RdbmsCatalogue.hpp b/catalogue/RdbmsCatalogue.hpp
index ea0044968364533c48318455d712cb1ee27b10fa..2a72b58c91fa0865017f1e12424a7e6a7eaa0428 100644
--- a/catalogue/RdbmsCatalogue.hpp
+++ b/catalogue/RdbmsCatalogue.hpp
@@ -839,6 +839,15 @@ public:
    */
   void checkTapeFileSearchCriteria(const TapeFileSearchCriteria &searchCriteria) const;
 
+    /**
+   * Throws a UserError exception if the specified searchCriteria is not valid
+   * due to a user error.
+   *
+   * @param conn The database connection.
+   * @param searchCriteria The search criteria.
+   */
+  void checkTapeFileSearchCriteria(rdbms::Conn &conn, const TapeFileSearchCriteria &searchCriteria) const;
+  
   /**
    * Returns the specified archive files.  Please note that the list of files
    * is ordered by archive file ID.
@@ -848,6 +857,17 @@ public:
    */
   ArchiveFileItor getArchiveFilesItor(const TapeFileSearchCriteria &searchCriteria) const override;
 
+
+  /**
+   * Returns the specified archive files.  Please note that the list of files
+   * is ordered by archive file ID.
+   *
+   * @param conn The database connection.
+   * @param searchCriteria The search criteria.
+   * @return The archive files.
+   */
+  ArchiveFileItor getArchiveFilesItor(rdbms::Conn &conn, const TapeFileSearchCriteria &searchCriteria) const;
+
   /**
    * Throws a UserError exception if the specified searchCriteria is not valid
    * due to a user error.
@@ -864,6 +884,13 @@ public:
    */
   FileRecycleLogItor getFileRecycleLogItor(const RecycleTapeFileSearchCriteria & searchCriteria) const override;
   
+  /**
+   * Restores the deleted files in the Recycle log that match the criteria passed
+   *
+   * @param searchCriteria The search criteria
+   */
+  void restoreFilesInRecycleLog(const RecycleTapeFileSearchCriteria & searchCriteria) override;
+
   /**
    * Returns the specified files in tape file sequence order.
    *
@@ -1923,6 +1950,14 @@ protected:
    */
   virtual void copyArchiveFileToFileRecyleLogAndDelete(rdbms::Conn & conn,const common::dataStructures::DeleteArchiveRequest &request, log::LogContext & lc) = 0;
   
+  /**
+   * Copy the fileRecycleLog to the TAPE_FILE table and deletes the corresponding FILE_RECYCLE_LOG table entry
+   * @param conn the database connection
+   * @param fileRecycleLog the fileRecycleLog we want to restore
+   * @param lc the log context
+   */
+  virtual void restoreFileCopiesInRecycleLog(rdbms::Conn & conn, FileRecycleLogItor &fileRecycleLogItor, log::LogContext & lc) = 0;
+  
   /**
    * Copies the ARCHIVE_FILE and TAPE_FILE entries to the recycle-bin tables 
    * @param conn the database connection
@@ -2038,6 +2073,8 @@ protected:
    */
   void deleteTapeFilesFromRecycleBin(rdbms::Conn & conn, const uint64_t archiveFileId);
   
+  void deleteTapeFileCopyFromRecycleBin(cta::rdbms::Conn & conn, const common::dataStructures::FileRecycleLog fileRecycleLog);
+
   /**
    * Delete the archive file from the ARCHIVE_FILE recycle-bin
    * @param conn the database connection
diff --git a/catalogue/RdbmsCatalogueGetArchiveFilesItor.cpp b/catalogue/RdbmsCatalogueGetArchiveFilesItor.cpp
index bef739104e2ebef88b109dd4426c7a32ad8a1acd..81c9ac8544cbd56c7698c1c9f649a5a8e8171e78 100644
--- a/catalogue/RdbmsCatalogueGetArchiveFilesItor.cpp
+++ b/catalogue/RdbmsCatalogueGetArchiveFilesItor.cpp
@@ -142,9 +142,13 @@ RdbmsCatalogueGetArchiveFilesItor::RdbmsCatalogueGetArchiveFilesItor(
       addedAWhereConstraint = true;
     }
 
-    // Order by FSEQ if we are listing the contents of a tape, else order by archive file ID
+    // Order by FSEQ if we are listing the contents of a tape, 
+    // by DISK_FILE_ID if listing the contents of a DISK_INSTANCE 
+    // else order by archive file ID
     if(searchCriteria.vid) {
       sql += " ORDER BY FSEQ";
+    } else if (searchCriteria.diskInstance) {
+      sql += " ORDER BY DISK_FILE_ID";
     } else {
       sql += " ORDER BY ARCHIVE_FILE_ID, COPY_NB";
     }
diff --git a/catalogue/SqliteCatalogue.cpp b/catalogue/SqliteCatalogue.cpp
index f235c0279b3e224e75d9b0ea14512e7373091b3b..a3f3b98c7a30adc3823ca61e359d5b6b3729213d 100644
--- a/catalogue/SqliteCatalogue.cpp
+++ b/catalogue/SqliteCatalogue.cpp
@@ -685,5 +685,95 @@ void SqliteCatalogue::copyTapeFileToFileRecyleLogAndDelete(rdbms::Conn & conn, c
   } 
 }
 
+//------------------------------------------------------------------------------
+// restoreFileCopiesInRecycleLog
+//------------------------------------------------------------------------------
+void SqliteCatalogue::restoreFileCopiesInRecycleLog(rdbms::Conn & conn, FileRecycleLogItor &fileRecycleLogItor, log::LogContext & lc) {
+try {
+    utils::Timer t;
+    log::TimingList tl;
+
+    //put fileRecycleLogs in std::list so we can release the underlying fileRecycleLogItor database connection
+    //otherwise we are using two conns when calling getArchiveFilesItor
+    std::list<common::dataStructures::FileRecycleLog> fileRecycleLogList;
+    while (fileRecycleLogItor.hasMore()) {
+      auto fileRecycleLog = fileRecycleLogItor.next();
+      fileRecycleLogList.push_back(fileRecycleLog);
+    }
+
+    //We currently do all file copy restoring in a single transaction
+    conn.executeNonQuery("BEGIN TRANSACTION");
+    for (auto &fileRecycleLog: fileRecycleLogList) {     
+      TapeFileSearchCriteria searchCriteria;
+      searchCriteria.archiveFileId = fileRecycleLog.archiveFileId;
+      searchCriteria.diskInstance = fileRecycleLog.diskInstanceName;
+      searchCriteria.diskFileIds = std::vector<std::string>();
+      searchCriteria.diskFileIds.value().push_back(fileRecycleLog.diskFileId);
+
+      auto itor = getArchiveFilesItor(conn, searchCriteria); 
+      if (itor.hasMore()) {
+        //only restore file copies, do nothing if file has been completely deleted in CTA
+        cta::common::dataStructures::ArchiveFile archiveFile = itor.next();
+        if (archiveFile.tapeFiles.find(fileRecycleLog.copyNb) != archiveFile.tapeFiles.end()) {
+          //copy with same copy_nb exists, cannot restore
+          UserSpecifiedExistingDeletedFileCopy ex;
+          ex.getMessage() << "Cannot restore file copy with archiveFileId " << std::to_string(fileRecycleLog.archiveFileId) 
+          << " and copy_nb " << std::to_string(fileRecycleLog.copyNb) << " because a tapefile with same archiveFileId and copy_nb already exists";
+          throw ex;
+        }
+        restoreFileCopyInRecycleLog(conn, fileRecycleLog, lc);
+      }
+    }
+    conn.commit();
+
+    log::ScopedParamContainer spc(lc);
+    tl.insertAndReset("commitTime",t);
+    tl.addToLog(spc);
+    lc.log(log::INFO,"In SqliteCatalogue::restoreFileCopiesInRecycleLog: all file copies successfully restored.");
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
+//------------------------------------------------------------------------------
+// restoreFileCopyInRecycleLog
+//------------------------------------------------------------------------------
+void SqliteCatalogue::restoreFileCopyInRecycleLog(rdbms::Conn &conn, const common::dataStructures::FileRecycleLog &fileRecycleLog, log::LogContext & lc) {
+  try {
+    utils::Timer t;
+    log::TimingList tl;
+    cta::common::dataStructures::TapeFile tapeFile;
+    tapeFile.vid = fileRecycleLog.vid;
+    tapeFile.fSeq = fileRecycleLog.fSeq;
+    tapeFile.copyNb = fileRecycleLog.copyNb;
+    tapeFile.blockId = fileRecycleLog.blockId;
+    tapeFile.fileSize = fileRecycleLog.sizeInBytes;
+    tapeFile.creationTime = fileRecycleLog.tapeFileCreationTime;
+
+    insertTapeFile(conn, tapeFile, fileRecycleLog.archiveFileId);
+    tl.insertAndReset("insertTapeFileTime",t);
+    
+    deleteTapeFileCopyFromRecycleBin(conn, fileRecycleLog);
+    tl.insertAndReset("deleteTapeFileCopyFromRecycleBinTime",t);
+    
+    log::ScopedParamContainer spc(lc);
+    spc.add("vid", tapeFile.vid);
+    spc.add("archiveFileId", fileRecycleLog.archiveFileId);
+    spc.add("fSeq", tapeFile.fSeq);
+    spc.add("copyNb", tapeFile.copyNb);
+    spc.add("fileSize", tapeFile.fileSize);
+    tl.addToLog(spc);
+    lc.log(log::INFO,"In SqliteCatalogue::restoreFileCopyInRecycleLog: File restored from the recycle log.");
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
 } // namespace catalogue
 } // namespace cta
diff --git a/catalogue/SqliteCatalogue.hpp b/catalogue/SqliteCatalogue.hpp
index 4102a8532ba2b638de1643b88859246f99aed3aa..70394195df73617de384388c2f98d67583983fb0 100644
--- a/catalogue/SqliteCatalogue.hpp
+++ b/catalogue/SqliteCatalogue.hpp
@@ -227,6 +227,24 @@ protected:
   void copyTapeFileToFileRecyleLogAndDelete(rdbms::Conn & conn, const cta::common::dataStructures::ArchiveFile &file, 
                                             const std::string &reason, log::LogContext & lc) override;
 
+  /**
+   * Copy the files in fileRecycleLogItor to the TAPE_FILE table and deletes the corresponding FILE_RECYCLE_LOG table entries
+   * @param conn the database connection
+   * @param fileRecycleLogItor the collection of fileRecycleLogs we want to restore
+   * @param lc the log context
+   */
+  void restoreFileCopiesInRecycleLog(rdbms::Conn & conn, FileRecycleLogItor &fileRecycleLogItor, log::LogContext & lc) override;
+
+  /**
+   * Copy the fileRecycleLog to the TAPE_FILE table and deletes the corresponding FILE_RECYCLE_LOG table entry
+   * @param conn the database connection
+   * @param fileRecycleLog the fileRecycleLog we want to restore
+   * @param lc the log context
+   */
+  void restoreFileCopyInRecycleLog(rdbms::Conn & conn, const common::dataStructures::FileRecycleLog &fileRecycleLogItor, log::LogContext & lc);
+
+
+
 private:
 
   /**
diff --git a/cmdline/CtaAdminCmd.cpp b/cmdline/CtaAdminCmd.cpp
index 334e55dc4ddb09edd295d03c4ed47977f66d947e..eb600d7df98b7d5d967ea15a43f604d2f37e0d1b 100644
--- a/cmdline/CtaAdminCmd.cpp
+++ b/cmdline/CtaAdminCmd.cpp
@@ -34,6 +34,8 @@ std::atomic<bool> isHeaderSent(false);
 cta::admin::TextFormatter formattedText(1000);
 
 
+std::string tp_config_file = "/etc/cta/TPCONFIG";
+
 namespace XrdSsiPb {
 
 /*!
@@ -229,7 +231,7 @@ void CtaAdminCmd::send() const
    } catch(std::runtime_error &ex) {
       throwUsage(ex.what());
    }
-   
+
    // Set configuration options
    const std::string config_file = "/etc/cta/cta-cli.conf";
    XrdSsiPb::Config config(config_file, "cta");
@@ -346,7 +348,36 @@ void CtaAdminCmd::parseOptions(int start, int argc, const char *const *const arg
    }
 }
 
+std::string CtaAdminCmd::getDriveFromTpConfig() {
+   std::ifstream file(tp_config_file);
+   if (file.fail()) {
+      throw std::runtime_error("Unable to open file " + tp_config_file);
+   }
+
+   std::string line;
 
+   while(std::getline(file, line)) {
+      // Strip out comments
+      auto pos = line.find('#');
+      if(pos != std::string::npos) {
+         line.resize(pos);
+      }
+
+      // Extract the list items
+      std::stringstream ss(line);
+      while(!ss.eof()) {
+         std::string item;
+         ss >> item;
+         // skip blank lines or lines consisting only of whitespace
+         if(item.empty()) continue;
+
+         std::string drivename = item.substr(0, item.find(" ")); // first word of line
+         return drivename;
+         
+      }
+   }
+   throw std::runtime_error("File " + tp_config_file + " is empty");
+}
 
 void CtaAdminCmd::addOption(const Option &option, const std::string &value)
 {
@@ -359,7 +390,11 @@ void CtaAdminCmd::addOption(const Option &option, const std::string &value)
          auto key = strOptions.at(option.get_key());
          auto new_opt = admincmd_ptr->add_option_str();
          new_opt->set_key(key);
-         new_opt->set_value(value);
+         if (option == opt_drivename_cmd && value == "first") {
+            new_opt->set_value(getDriveFromTpConfig());
+         } else {
+            new_opt->set_value(value);
+         }
          break;
       }
       case Option::OPT_STR_LIST: {
diff --git a/cmdline/CtaAdminCmd.hpp b/cmdline/CtaAdminCmd.hpp
index fa5bfee36f7be15aa64a1c59f455859a93381966..3c53f016f0551d242c1314599780895fb4ce9fe5 100644
--- a/cmdline/CtaAdminCmd.hpp
+++ b/cmdline/CtaAdminCmd.hpp
@@ -49,6 +49,9 @@ private:
    //! Add a valid option to the protocol buffer
    void addOption(const Option &option, const std::string &value);
 
+   //! Parses the TPCONFIG file to obtain the first drive in the tapeserver
+   std::string getDriveFromTpConfig();
+
    //! Read a list of string options from a file
    void readListFromFile(cta::admin::OptionStrList &str_list, const std::string &filename);
 
diff --git a/cmdline/CtaAdminCmdParse.hpp b/cmdline/CtaAdminCmdParse.hpp
index 3d4d1a377989c2e06e943eb6e4330d4cf8517dcf..51bb8cc7b9f9bc5182e9211f351eec4b748b529e 100644
--- a/cmdline/CtaAdminCmdParse.hpp
+++ b/cmdline/CtaAdminCmdParse.hpp
@@ -66,6 +66,13 @@ public:
       return option == m_short_opt || option == m_long_opt;
    }
 
+   /*!
+    * Check if the supplied option matches the option
+    */
+   bool operator==(const Option &option) const {
+      return option == m_short_opt || option == m_long_opt;
+   }
+
    /*!
     * Return the type of this option
     */
@@ -238,6 +245,7 @@ const subcmdLookup_t subcmdLookup = {
    { "rm",                      AdminCmd::SUBCMD_RM },
    { "up",                      AdminCmd::SUBCMD_UP },
    { "down",                    AdminCmd::SUBCMD_DOWN },
+   { "restore",                 AdminCmd::SUBCMD_RESTORE }
 };
 
 
@@ -345,7 +353,9 @@ const std::map<AdminCmd::Cmd, CmdHelp> cmdHelp = {
                         "\n  This is a synchronous command that sets and reads back the state of one or\n"
                           "  more drives. The <drive_name> option accepts a regular expression. If the\n"
                           "  --force option is not set, the drives will complete any running mount and\n"
-                          "  drives must be in the down state before deleting.\n\n"
+                          "  drives must be in the down state before deleting. If the <drive_name> option\n"
+                          "  is set to first, the up, down, ls and ch commands will use the first drive\n"
+                          "  listed in TPCONFIG\n\n"
                                          }},
    { AdminCmd::CMD_FAILEDREQUEST,        { "failedrequest",        "fr",  { "ls", "rm" } }},
    { AdminCmd::CMD_GROUPMOUNTRULE,       { "groupmountrule",       "gmr", { "add", "ch", "rm", "ls" } }},
@@ -409,9 +419,11 @@ const std::map<AdminCmd::Cmd, CmdHelp> cmdHelp = {
                                          }},
    { AdminCmd::CMD_VERSION,              { "version",               "v",  { } }},
    { AdminCmd::CMD_SCHEDULINGINFOS,      { "schedulinginfo",        "si",  { "ls" } }},
-   { AdminCmd::CMD_RECYCLETAPEFILE,      { "recycletf",        "rtf",  { "ls" },
+   { AdminCmd::CMD_RECYCLETAPEFILE,      { "recycletf",        "rtf",  { "ls", "restore" },
+                            "  This command allows to manage files in the recycle log.\n"
                             "  Tape files in the recycle log can be listed by VID, EOS disk file ID, EOS disk instance, ArchiveFileId or copy number.\n"
-                            "  Disk file IDs should be provided in hexadecimal (fxid).\n\n" }},
+                            "  Disk file IDs should be provided in hexadecimal (fxid).\n"
+                            "  Deleted files can be restored with the restore command\n\n" }},
 };
 
 
@@ -429,7 +441,6 @@ const Option opt_comment              { Option::OPT_STR,  "--comment",
 const Option opt_copynb               { Option::OPT_UINT, "--copynb",                "-c",   " <copy_number>" };
 const Option opt_copynb_alias         { Option::OPT_UINT, "--numberofcopies",        "-c",   " <number_of_copies>", "--copynb" };
 const Option opt_disabled             { Option::OPT_BOOL, "--disabled",              "-d",   " <\"true\" or \"false\">" };
-const Option opt_drivename            { Option::OPT_STR,  "--drive",                 "-d",   " <drive_name>" };
 const Option opt_drivename_cmd        { Option::OPT_CMD,  "--drive",                 "",     "<drive_name>" };
 const Option opt_encrypted            { Option::OPT_BOOL, "--encrypted",             "-e",   " <\"true\" or \"false\">" };
 const Option opt_encryptionkeyname    { Option::OPT_STR,  "--encryptionkeyname",     "-k",   " <encryption_key_name>" };
@@ -508,8 +519,8 @@ const std::map<cmd_key_t, cmd_val_t> cmdOptions = {
    /*----------------------------------------------------------------------------------------------------*/
    {{ AdminCmd::CMD_DRIVE,                AdminCmd::SUBCMD_UP    }, { opt_drivename_cmd, opt_reason.optional() }},
    {{ AdminCmd::CMD_DRIVE,                AdminCmd::SUBCMD_DOWN  }, { opt_drivename_cmd, opt_reason, opt_force_flag.optional() }},
-   {{ AdminCmd::CMD_DRIVE,                AdminCmd::SUBCMD_LS    }, { opt_drivename.optional() }},
-   {{ AdminCmd::CMD_DRIVE,                AdminCmd::SUBCMD_RM    }, { opt_drivename_cmd, opt_force_flag.optional() }},
+   {{ AdminCmd::CMD_DRIVE,                AdminCmd::SUBCMD_LS    }, { opt_drivename_cmd.optional() }},
+   {{ AdminCmd::CMD_DRIVE,                AdminCmd::SUBCMD_RM    }, { opt_drivename_cmd, opt_force_flag.optional()}},
    {{ AdminCmd::CMD_DRIVE,                AdminCmd::SUBCMD_CH    }, { opt_drivename_cmd, opt_comment }},
    /*----------------------------------------------------------------------------------------------------*/
    {{ AdminCmd::CMD_FAILEDREQUEST,        AdminCmd::SUBCMD_LS    },
@@ -620,6 +631,8 @@ const std::map<cmd_key_t, cmd_val_t> cmdOptions = {
    {{ AdminCmd::CMD_SCHEDULINGINFOS,      AdminCmd::SUBCMD_LS   }, { }},
    {{ AdminCmd::CMD_RECYCLETAPEFILE, AdminCmd::SUBCMD_LS }, 
    { opt_vid.optional(), opt_fid.optional(), opt_fidfile.optional(), opt_copynb.optional(), opt_archivefileid.optional(), opt_instance.optional() }},
+   {{ AdminCmd::CMD_RECYCLETAPEFILE, AdminCmd::SUBCMD_RESTORE }, 
+   { opt_vid.optional(), opt_fid.optional(), opt_fidfile.optional(), opt_copynb.optional(), opt_archivefileid.optional(), opt_instance.optional() }},
 };
 
 
@@ -630,5 +643,4 @@ const std::map<cmd_key_t, cmd_val_t> cmdOptions = {
  * Throws a std::runtime_error if the command is invalid
  */
 void validateCmd(const cta::admin::AdminCmd &admincmd);
-
 }} // namespace cta::admin
diff --git a/xroot_plugins/XrdCtaTapeFileLs.hpp b/xroot_plugins/XrdCtaTapeFileLs.hpp
index a90582ebfa3419cea7e735ed6071fdce597063ca..ca813935ca409d01424f53514348b619e70aba8a 100644
--- a/xroot_plugins/XrdCtaTapeFileLs.hpp
+++ b/xroot_plugins/XrdCtaTapeFileLs.hpp
@@ -94,8 +94,7 @@ TapeFileLsStream::TapeFileLsStream(const RequestMessage &requestMsg,
     }
     searchCriteria.diskFileIds->push_back(std::to_string(fid));
   }
-  // Disk instance on its own does not give a valid set of search criteria (no &has_any)
-  searchCriteria.diskInstance = requestMsg.getOptional(OptionString::INSTANCE);
+  searchCriteria.diskInstance = requestMsg.getOptional(OptionString::INSTANCE, &has_any);
 
   searchCriteria.archiveFileId = requestMsg.getOptional(OptionUInt64::ARCHIVE_FILE_ID, &has_any);
 
diff --git a/xroot_plugins/XrdSsiCtaRequestMessage.cpp b/xroot_plugins/XrdSsiCtaRequestMessage.cpp
index 43a403e1142e827bf245b998b376bf2874ec6a97..265c4a048e6bfc85c381a97407d8eb0d44ae5514 100644
--- a/xroot_plugins/XrdSsiCtaRequestMessage.cpp
+++ b/xroot_plugins/XrdSsiCtaRequestMessage.cpp
@@ -282,6 +282,10 @@ void RequestMessage::process(const cta::xrd::Request &request, cta::xrd::Respons
            case cmd_pair(AdminCmd::CMD_RECYCLETAPEFILE, AdminCmd::SUBCMD_LS):
                processRecycleTapeFile_Ls(response,stream);
                break;
+           case cmd_pair(AdminCmd::CMD_RECYCLETAPEFILE, AdminCmd::SUBCMD_RESTORE):
+               processRecycleTapeFile_Restore(response);
+               break;
+           
             default:
                throw PbException("Admin command pair <" +
                      AdminCmd_Cmd_Name(request.admincmd().cmd()) + ", " +
@@ -2146,4 +2150,39 @@ void RequestMessage::processRecycleTapeFile_Ls(cta::xrd::Response &response, Xrd
   response.set_type(cta::xrd::Response::RSP_SUCCESS);
 }
 
+void RequestMessage::processRecycleTapeFile_Restore(cta::xrd::Response& response) {
+   response.set_type(cta::xrd::Response::RSP_SUCCESS);
+   using namespace cta::admin;
+
+  bool has_any = false;
+  cta::catalogue::RecycleTapeFileSearchCriteria searchCriteria;
+  
+  searchCriteria.vid = getOptional(OptionString::VID, &has_any);  
+  auto diskFileId = getOptional(OptionString::FXID, &has_any);
+  searchCriteria.diskFileIds = getOptional(OptionStrList::FILE_ID, &has_any);
+  
+  if(diskFileId){
+    // single option on the command line we need to do the conversion ourselves.
+    if(!searchCriteria.diskFileIds) searchCriteria.diskFileIds = std::vector<std::string>();
+
+    auto fid = strtol(diskFileId->c_str(), nullptr, 16);
+    if(fid < 1 || fid == LONG_MAX) {
+       throw cta::exception::UserError(*diskFileId + " is not a valid file ID");
+    }
+
+    searchCriteria.diskFileIds->push_back(std::to_string(fid));
+  }
+  // Disk instance on its own does not give a valid set of search criteria (no &has_any)
+  searchCriteria.diskInstance = getOptional(OptionString::INSTANCE);
+  searchCriteria.archiveFileId = getOptional(OptionUInt64::ARCHIVE_FILE_ID, &has_any);
+  // Copy number on its own does not give a valid set of search criteria (no &has_any)
+  searchCriteria.copynb = getOptional(OptionUInt64::COPY_NUMBER);
+
+  if(!has_any){
+    throw cta::exception::UserError("Must specify at least one search option");
+  }
+   m_catalogue.restoreFilesInRecycleLog(searchCriteria);
+  response.set_type(cta::xrd::Response::RSP_SUCCESS);
+}
+
 }} // namespace cta::xrd
diff --git a/xroot_plugins/XrdSsiCtaRequestMessage.hpp b/xroot_plugins/XrdSsiCtaRequestMessage.hpp
index fb9b2f4f27b2eda48cb1a1db3992f2010c1c35e3..616516bcffb2c4851334ba0f580e9f2f2388eaca 100644
--- a/xroot_plugins/XrdSsiCtaRequestMessage.hpp
+++ b/xroot_plugins/XrdSsiCtaRequestMessage.hpp
@@ -208,6 +208,7 @@ private:
   void processVirtualOrganization_Add(cta::xrd::Response &response);
   void processVirtualOrganization_Ch(cta::xrd::Response &response);
   void processVirtualOrganization_Rm(cta::xrd::Response &response);
+  void processRecycleTapeFile_Restore(cta::xrd::Response &response);
   
   /*!
    * Process AdminCmd events which can return a stream response
diff --git a/xrootd-ssi-protobuf-interface b/xrootd-ssi-protobuf-interface
index 2f8068955f6622f26e6373a70cfcb4f16435d7ea..3ebc433ff481ebf8fc8cfcaf8dc3dff608fceee0 160000
--- a/xrootd-ssi-protobuf-interface
+++ b/xrootd-ssi-protobuf-interface
@@ -1 +1 @@
-Subproject commit 2f8068955f6622f26e6373a70cfcb4f16435d7ea
+Subproject commit 3ebc433ff481ebf8fc8cfcaf8dc3dff608fceee0