Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Limiting Generations in Windows 10 FileHistory/Backup

4.20/5 (2 votes)
16 Aug 2017CPOL2 min read 8.1K   5  
Prevent bloat in the new Windows backup mechanism

Introduction

The new Windows FileHistory achieves its goal of being an easy to set up archiving and backup mechanism needing minimal intervention to achieve a fairly sophisticated multi-generation file history. The only problem is that, using the default "Back up my files" setting of one hour, the number of items rapidly escalates for files which are constantly updated. This little utility lets you periodically trim and retain only the last n number of files on your FileHistory/backup sets.

The Code

This article is intended as a useful utility rather than as a demontration of any fancy coding techniques. This is the first time I have submitted code. I must say that the thought of other people looking at ones code tends to focus the mind. Instead of the usual quick and dirty approach I found myself going the extra mile to do things "properly", do some error checking and even the occasional comment.

One feature needs some explanation. The intuitive way to find which files to purge would be to sort the files and then scan through looking for duplicates of the original fileneme i.e. the part before "(archive date)". The problem is that the suffix (e.g. ".txt") is not taken into account and files with the same basic name but different suffixes will all be lumped together for the purpose of deciding how many are present. I got around this by creating a sort array consisting of:

  • The suffix
  • a placemarker '*' (used because it can never be part of a filename or directory name)
  • the full path

The array is then sorted into descending sequence so that the newest files are first. The 'compare name' is that text before the last '(' and  the original path/filename is the text after '*'. This technique will fail in the fairly obscure case where the suffix contains brackets - there will always be limits.

I would like to add functionality where instead of, or in addition to,  a number of generations, the FileHistory consists of, say, one file that is at least a month old, one file at least a week and one at least a day. Any bright young minds with nothing to do are welcome to have a go.

I would really value comments and perhaps suggested alternatives to the way this old guy does things.

C#
// Eliminate old files from Windows 10 File History

//The command line parameters are:
//1 Root directory of target
//2 Number of generations to retain

// Note that the Read-only attribute set by Windows is ignored & overridden to allow deletion

// The program is intended to run in batch mode and so errors are listed without intervention

using System;
using System.IO;
using System.Text;
using System.Collections;

public class RecursiveFileProcessor
{
    public static void Main(string[] args)
    {
        int MaxRetain;
        if (args.Length != 2 || !int.TryParse(args[1], out MaxRetain))
        {
            Console.WriteLine("Usage - " + System.AppDomain.CurrentDomain.FriendlyName + " Path NumberOfGenerations ");
            Console.WriteLine("For example " + System.AppDomain.CurrentDomain.FriendlyName + " C:\\FileHistory 2");
            return;
        }
        if (Directory.Exists(args[0]))
        {
            // This path is a directory
            ProcessDirectory(args[0], MaxRetain);
        }
        else
        {
            Console.WriteLine("{0} is not available or is not a valid directory.", args[0]);
        }
    }

    // Process all files in the directory passed in, recurse on any directories
    // that are found, and process the files they contain.
    public static void ProcessDirectory(string targetDirectory, int MaxRetain)
    {
        string[] fileEntries = Directory.GetFiles(targetDirectory);
        string[] SortEntries = new string[fileEntries.Length]; // copy entries but prepend with
            // suffix (e.g. .txt) and a placemarker '*' This character was chosen because it cannot be part of filename
                // all this is to prevent files with same name but different suffixes from being treated
                // as if they form part of the same history set
        if (SortEntries.Length > 0)
        {
            int x = 0;
            foreach (string fileName in fileEntries)
            {
                int LastClose = fileName.LastIndexOf(')');
                int LenSuffix = fileName.Length - LastClose - 1;
                SortEntries[x] = "";
                if (LastClose > 0 && LenSuffix > 0)
                    SortEntries[x] = fileName.Substring(fileName.Length - LenSuffix);
                SortEntries[x] = SortEntries[x] + '*';
                SortEntries[x] = SortEntries[x] + fileName;
                x++;
            }
            Array.Sort(SortEntries);
            Array.Reverse(SortEntries); // newest files first within set
            string PrevName = "";
            int CountDup = 0;
            foreach (string SortFile in SortEntries)
            {
                if (SortFile == null) continue;
                string OriginalName = SortFile.Substring(SortFile.IndexOf('*') + 1);
                int OpenPos = SortFile.LastIndexOf('(');
                string CompareName = SortFile;
                if (OpenPos > 0)
                {
                    CompareName = SortFile.Substring(0, SortFile.LastIndexOf('('));
                }
                if (CompareName == PrevName)
                {
                    CountDup++;
                }
                else
                {
                    CountDup = 0;
                    PrevName = CompareName;
                }
                if (CountDup >= MaxRetain)
                {
                    try
                    {
                        FileAttributes attributes = File.GetAttributes(OriginalName);
                        File.SetAttributes(OriginalName, attributes &  ~FileAttributes.ReadOnly); // remove Read Only
                        File.Delete(OriginalName);
                        Console.WriteLine("{0} deleted", OriginalName);
                    }
                    catch(Exception e) // this will handle sharing violations and permission issues
                    {
                        Console.WriteLine( "Unable to delete {0} because {1}", OriginalName, e);
                        continue;
                    }
                }

            }
        }
        // Recurse into subdirectories of this directory.
        string[] subdirectoryEntries = Directory.GetDirectories(targetDirectory);
        foreach (string subdirectory in subdirectoryEntries)
            ProcessDirectory(subdirectory, MaxRetain);
    }
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)