| 
					
				 | 
			
			
				@@ -30,15 +30,62 @@ def prune_within(archives, hours, kept_because): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     return result 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+def default_period_func(pattern): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    def inner(a): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # compute in local timezone 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return a.ts.astimezone().strftime(pattern) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return inner 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+def quarterly_13weekly_period_func(a): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    (year, week, _) = a.ts.astimezone().isocalendar()  # local time 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    if week <= 13: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # Weeks containing Jan 4th to Mar 28th (leap year) or 29th- 91 (13*7) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # days later. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return (year, 1) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    elif 14 <= week <= 26: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # Weeks containing Apr 4th (leap year) or 5th to Jun 27th or 28th- 91 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # days later. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return (year, 2) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    elif 27 <= week <= 39: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # Weeks containing Jul 4th (leap year) or 5th to Sep 26th or 27th- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # at least 91 days later. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return (year, 3) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    else: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # Everything else, Oct 3rd (leap year) or 4th onward, will always 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # include week of Dec 26th (leap year) or Dec 27th, may also include 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # up to possibly Jan 3rd of next year. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return (year, 4) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+def quarterly_3monthly_period_func(a): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    lt = a.ts.astimezone()  # local time 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    if lt.month <= 3: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # 1-1 to 3-31 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return (lt.year, 1) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    elif 4 <= lt.month <= 6: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # 4-1 to 6-30 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return (lt.year, 2) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    elif 7 <= lt.month <= 9: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # 7-1 to 9-30 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return (lt.year, 3) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    else: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # 10-1 to 12-31 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return (lt.year, 4) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 PRUNING_PATTERNS = OrderedDict( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     [ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        ("secondly", "%Y-%m-%d %H:%M:%S"), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        ("minutely", "%Y-%m-%d %H:%M"), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        ("hourly", "%Y-%m-%d %H"), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        ("daily", "%Y-%m-%d"), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        ("weekly", "%G-%V"), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        ("monthly", "%Y-%m"), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        ("yearly", "%Y"), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ("secondly", default_period_func("%Y-%m-%d %H:%M:%S")), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ("minutely", default_period_func("%Y-%m-%d %H:%M")), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ("hourly", default_period_func("%Y-%m-%d %H")), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ("daily", default_period_func("%Y-%m-%d")), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ("weekly", default_period_func("%G-%V")), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ("monthly", default_period_func("%Y-%m")), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ("quarterly_13weekly", quarterly_13weekly_period_func), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ("quarterly_3monthly", quarterly_3monthly_period_func), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ("yearly", default_period_func("%Y")), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     ] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -46,7 +93,7 @@ PRUNING_PATTERNS = OrderedDict( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 def prune_split(archives, rule, n, kept_because=None): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     last = None 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     keep = [] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-    pattern = PRUNING_PATTERNS[rule] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    period_func = PRUNING_PATTERNS[rule] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     if kept_because is None: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         kept_because = {} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     if n == 0: 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -54,8 +101,7 @@ def prune_split(archives, rule, n, kept_because=None): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     a = None 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     for a in sorted(archives, key=attrgetter("ts"), reverse=True): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        # we compute the pruning in local time zone 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        period = a.ts.astimezone().strftime(pattern) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        period = period_func(a) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         if period != last: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             last = period 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             if a.id not in kept_because: 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -75,12 +121,24 @@ class PruneMixIn: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     def do_prune(self, args, repository, manifest): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         """Prune repository archives according to specified rules""" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         if not any( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            (args.secondly, args.minutely, args.hourly, args.daily, args.weekly, args.monthly, args.yearly, args.within) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            ( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                args.secondly, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                args.minutely, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                args.hourly, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                args.daily, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                args.weekly, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                args.monthly, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                args.quarterly_13weekly, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                args.quarterly_3monthly, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                args.yearly, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                args.within, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         ): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             raise CommandError( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 'At least one of the "keep-within", "keep-last", ' 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 '"keep-secondly", "keep-minutely", "keep-hourly", "keep-daily", ' 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                '"keep-weekly", "keep-monthly" or "keep-yearly" settings must be specified.' 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                '"keep-weekly", "keep-monthly", "keep-13weekly", "keep-3monthly", ' 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                'or "keep-yearly" settings must be specified.' 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         if args.format is not None: 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -190,10 +248,15 @@ class PruneMixIn: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         starts is used for pruning purposes. Dates and times are interpreted in the local 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         timezone of the system where borg prune runs, and weeks go from Monday to Sunday. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         Specifying a negative number of archives to keep means that there is no limit. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        As of borg 1.2.0, borg will retain the oldest archive if any of the secondly, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        minutely, hourly, daily, weekly, monthly, or yearly rules was not otherwise able to 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        meet its retention target. This enables the first chronological archive to continue 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        aging until it is replaced by a newer archive that meets the retention criteria. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        Borg will retain the oldest archive if any of the secondly, minutely, hourly, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        daily, weekly, monthly, quarterly, or yearly rules was not otherwise able to 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        meet its retention target. This enables the first chronological archive to 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        continue aging until it is replaced by a newer archive that meets the retention 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        criteria. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        The ``--keep-13weekly`` and ``--keep-3monthly`` rules are two different 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        strategies for keeping archives every quarter year. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         The ``--keep-last N`` option is doing the same as ``--keep-secondly N`` (and it will 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         keep the last N archives under the assumption that you do not create more than one 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -293,6 +356,21 @@ class PruneMixIn: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             action=Highlander, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             help="number of monthly archives to keep", 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        quarterly_group = subparser.add_mutually_exclusive_group() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        quarterly_group.add_argument( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            "--keep-13weekly", 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            dest="quarterly_13weekly", 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            type=int, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            default=0, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            help="number of quarterly archives to keep (13 week strategy)", 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        quarterly_group.add_argument( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            "--keep-3monthly", 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            dest="quarterly_3monthly", 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            type=int, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            default=0, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            help="number of quarterly archives to keep (3 month strategy)", 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         subparser.add_argument( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             "-y", 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             "--keep-yearly", 
			 |