5252
5353BREACHWATCH_MAX = 5
5454KEEPER_API_BATCH_LIMIT = 999 # Maximum objects per pre_delete API request
55+ KEEPER_API_FOLDER_BATCH_LIMIT = 500 # Maximum folders per pre_delete API request
5556
5657is_windows = sys .platform .startswith ('win' )
5758
@@ -634,7 +635,14 @@ def print_device_info(params: KeeperParams):
634635
635636class RecordDeleteAllCommand (Command ):
636637 """
637- Delete all records and folders from the vault.
638+ Delete all records and folders from the user vault.
639+
640+ IMPORTANT: This command automatically skips shared folders and their records.
641+ For shared folders, use the recommended workflow:
642+ 1. 'transform-folder <folder_name>' to move records to user folders (fast)
643+ 2. Run delete-all to clean remaining user vault content
644+
645+ The command will alert you about any skipped shared folder content.
638646
639647 Args:
640648 params: KeeperParams instance
@@ -643,9 +651,10 @@ class RecordDeleteAllCommand(Command):
643651 Returns:
644652 None
645653
646- Note:
647- Records in multiple folders will be processed using the first
648- folder found. Deletions are processed in batches of 999 items.
654+ Note:
655+ Only processes user folders and records. Uses batches of up to 999 items per
656+ API call. Records in multiple folders will be processed using the first user
657+ folder found.
649658 """
650659
651660 def get_parser (self ):
@@ -699,15 +708,68 @@ def _confirm_user_wants_deletion(self, kwargs):
699708 return True
700709
701710 def _process_record_deletion (self , params ):
702- """Collect and delete all records."""
703- records_with_folders = self ._collect_records_with_folders (params )
711+ """Collect and delete all records, avoiding shared folder records ."""
712+ records_with_folders , skipped_stats = self ._collect_records_with_folders_safe (params )
704713 if not records_with_folders :
705714 logging .info ('No records found to delete' )
706715 return DeletionStats ()
707716
708- logging .info ('Preparing to delete %s records from Keeper' , len (records_with_folders ))
717+ if skipped_stats ['shared_folders' ] > 0 or skipped_stats ['shared_records' ] > 0 :
718+ print (f"\n SHARED FOLDER CONTENT SKIPPED:" )
719+ print (f" • { skipped_stats ['shared_folders' ]} shared folders avoided" )
720+ print (f" • { skipped_stats ['shared_records' ]} records in shared folders avoided" )
721+ print (f"\n For shared folders with many records, use this workflow:" )
722+ print (f" 1. Run 'transform-folder <shared_folder_uid>' to convert shared folder to user folder (fast)" )
723+ print (f" 2. Then run delete-all to clean remaining user vault content\n " )
724+
725+ logging .info ('Preparing to delete %s records from user folders' , len (records_with_folders ))
709726 return self ._delete_objects_in_batches (params , records_with_folders , 'records' )
710727
728+ def _collect_records_with_folders_safe (self , params ):
729+ """Collect records from user folders only, avoiding shared folders."""
730+ from ..subfolder import BaseFolderNode
731+
732+ safe_records = []
733+ skipped_stats = {
734+ 'shared_folders' : 0 ,
735+ 'shared_records' : 0
736+ }
737+
738+ shared_folder_uids = set ()
739+ shared_record_uids = set ()
740+
741+ for folder_uid , folder in params .folder_cache .items ():
742+ if folder .type in (BaseFolderNode .SharedFolderType , BaseFolderNode .SharedFolderFolderType ):
743+ shared_folder_uids .add (folder_uid )
744+ skipped_stats ['shared_folders' ] += 1
745+
746+ folder_records = params .subfolder_record_cache .get (folder_uid , set ())
747+ shared_record_uids .update (folder_records )
748+
749+ skipped_stats ['shared_records' ] = len (shared_record_uids )
750+
751+ for record_uid in params .record_cache :
752+ if record_uid in shared_record_uids :
753+ continue
754+
755+ record_folder = None
756+ for folder_uid in params .subfolder_record_cache :
757+ if record_uid in params .subfolder_record_cache .get (folder_uid , set ()):
758+ folder = params .folder_cache .get (folder_uid )
759+ if folder and folder .type == BaseFolderNode .UserFolderType :
760+ record_folder = folder
761+ break
762+
763+ if record_folder is None :
764+ root_records = params .subfolder_record_cache .get ('' , set ())
765+ if record_uid in root_records :
766+ record_folder = params .root_folder
767+
768+ if record_folder is not None :
769+ safe_records .append ((record_folder , record_uid ))
770+
771+ return safe_records , skipped_stats
772+
711773 def _collect_records_with_folders (self , params ):
712774 """Collect all records with their folder contexts."""
713775 from ..subfolder import find_all_folders
@@ -738,7 +800,7 @@ def _collect_records_with_folders(self, params):
738800 return records_with_folders
739801
740802 def _process_folder_deletion (self , params ):
741- """Delete all empty folders."""
803+ """Delete all empty user folders, avoiding shared folders."""
742804 return self ._delete_empty_folders (params )
743805
744806 def _delete_objects_in_batches (self , params , objects_with_context , object_type_name ):
@@ -863,7 +925,7 @@ def _finalize_deletion(self, params, record_stats, folder_stats):
863925
864926 def _delete_empty_folders (self , params ):
865927 """
866- Delete all empty folders after records have been deleted.
928+ Delete all empty user folders after records have been deleted.
867929
868930 Args:
869931 params: KeeperParams instance
@@ -872,9 +934,8 @@ def _delete_empty_folders(self, params):
872934 int: Number of folders successfully deleted
873935
874936 Note:
875- Only deletes folders that are completely empty (no records and no subfolders).
876- Root folder is never deleted. Folders are sorted by depth (deepest first)
877- to avoid parent-child dependency issues.
937+ Only deletes user folders that are completely empty.
938+ Skips shared folders entirely. Root folder is never deleted.
878939 """
879940 from ..subfolder import BaseFolderNode
880941
@@ -886,41 +947,46 @@ def _delete_empty_folders(self, params):
886947 # Sync down to get latest folder state after record deletion
887948 api .sync_down (params )
888949
889- # Find all folders that are safe to delete
890- empty_folders = []
950+ empty_user_folders = []
951+ skipped_shared_folders = 0
952+
891953 for folder_uid , folder in params .folder_cache .items ():
892954 # Security check: Skip if folder or folder_uid is invalid
893955 if not folder_uid or not folder or not hasattr (folder , 'type' ):
894956 logging .warning ("Skipping invalid folder entry" )
895957 continue
896958
897- # Security check: Do not attempt to delete root folder
898- # Delete all other folder types (user folders, shared folders, shared folder subfolders)
899959 if folder .type == BaseFolderNode .RootFolderType :
900960 continue
901961
902- # Check if folder is empty (no records)
903- records_in_folder = params .subfolder_record_cache .get (folder_uid , set ())
904- if len (records_in_folder ) == 0 :
905- # Also check if it has any subfolders - only delete if completely empty
906- if not folder .subfolders or len (folder .subfolders ) == 0 :
907- empty_folders .append (folder )
962+ if folder .type in (BaseFolderNode .SharedFolderType , BaseFolderNode .SharedFolderFolderType ):
963+ skipped_shared_folders += 1
964+ continue
965+
966+ if folder .type == BaseFolderNode .UserFolderType :
967+ records_in_folder = params .subfolder_record_cache .get (folder_uid , set ())
968+ if len (records_in_folder ) == 0 :
969+ if not folder .subfolders or len (folder .subfolders ) == 0 :
970+ empty_user_folders .append (folder )
971+
972+ if skipped_shared_folders > 0 :
973+ logging .info (f"Skipped { skipped_shared_folders } shared folders (use transform-folder workflow instead)" )
908974
909- if not empty_folders :
910- logging .info ("No folders found to delete" )
975+ if not empty_user_folders :
976+ logging .info ("No empty user folders found to delete" )
911977 return 0
912978
913- logging .info ('Found %s folders to delete' , len (empty_folders ))
979+ logging .info ('Found %s empty user folders to delete' , len (empty_user_folders ))
914980
915981 # Sort folders by depth (deepest first) to avoid parent-child dependency issues
916982 # Folders with longer paths are deeper in the hierarchy
917983 from ..subfolder import get_folder_path
918- empty_folders .sort (key = lambda f : len (get_folder_path (params , f .uid ).split ('/' )), reverse = True )
984+ empty_user_folders .sort (key = lambda f : len (get_folder_path (params , f .uid ).split ('/' )), reverse = True )
919985
920986 total_folders_deleted = 0
921987 total_folders_failed = 0
922988
923- folders_to_delete = empty_folders [:]
989+ folders_to_delete = empty_user_folders [:]
924990 while len (folders_to_delete ) > 0 :
925991 # Prepare pre_delete request for folders
926992 rq = {
@@ -929,8 +995,8 @@ def _delete_empty_folders(self, params):
929995 }
930996
931997 # Process chunk of folders
932- chunk = folders_to_delete [:KEEPER_API_BATCH_LIMIT ]
933- folders_to_delete = folders_to_delete [KEEPER_API_BATCH_LIMIT :]
998+ chunk = folders_to_delete [:KEEPER_API_FOLDER_BATCH_LIMIT ]
999+ folders_to_delete = folders_to_delete [KEEPER_API_FOLDER_BATCH_LIMIT :]
9341000
9351001 for folder in chunk :
9361002 # Security check: Validate folder has required attributes
0 commit comments