This C# console application is meant to be run as a scheduled task to periodically move or delete files from a specified location based upon the files last modified time. Optionally, it can move files that are older than the specified timespan to an alternate location, delete the files, or send them to the recycle bin.
I use this to manage a "Circular Folder" for mostly one-time use, short-lived files, downloads etc. As opposed to sending them directly to the recycle bin and not being able to control when exactly these files are deleted, I setup a structure to move files based upon their last modified time. Files older than 7 days can be moved to alternate directory:
AutoPrune.exe /older: 7 /prune:"D:\Circular Folder" /move: "D:\Circular Folder\7 Day" /exclude: "7 Day", "30 Day" /verbose AutoPrune.exe /older: 30 /prune:"D:\Circular Folder\7 Day" /move: "D:\Circular Folder\30 Day" /verbose AutoPrune.exe /older: 60 /prune:"D:\Circular Folder\30 Day" /recycle /verbose
The first command will move files located under the D:\Circular Folder directory with a last modified date older than 7 days to the "D:\Circular Folder\7 Day" folder. The exclude argument allows for excluding files and folders based upon regular expressions, but in this case, any files/folders with the names "7 Day" and "30 Day" will be excluded from processing. Otherwise, all files/folders recursively under the /prune folder will be processed, unless the /include argument is specified.
Likewise, the second command will move files from the 7 Day folder that are older than 30 days to the "30 Day" folder. And finally the third command will send files in the "30 Day" folder that are older than 60 days to the Recycle Bin.
/* Copyright (C) 2004 Andrew Loree
* $Id: AutoPrune.cs,v 1.3 2004/07/04 05:50:44 andy Exp $
****************************************************************************
* Main.cs - AutoPrune: Console application to manage download directories,
* and temp file locations, by deleting, recycling, or moving files
* based upon the last written (modified)/ or creation time (which ever
* is newer
****************************************************************************
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*****************************************************************************
* Version History:
* 1.0 - Initial Release
* 1.1 - Added Creation/Last Modified check
*****************************************************************************/
using System;
using System.IO;
using System.Collections;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
namespace AutoPrune
{
/// <summary>
/// Stores parsed program options
/// </summary>
class AutoPruneArgs
{
public DateTime dtOlderThan;
public string sPrunePath;
public PruneMethod eMethod = PruneMethod.None;
public string sMovePath = null;
public ArrayList aInclude = new ArrayList();
public ArrayList aExclude = new ArrayList();
public bool bRemoveEmptyDirs = true;
public bool bVerbose = false;
/// <summary>
/// Constructor
/// </summary>
public AutoPruneArgs()
{
dtOlderThan = DateTime.Now;
}
}
enum PruneMethod
{ None, Recycle, Delete, Move };
/// <summary>
/// Summary description for Class1.
/// </summary>
class CMain
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
AutoPruneArgs myArgs = new AutoPruneArgs();
// Parse command-line
if (!ParseCommandline(args,ref myArgs))
Environment.Exit(-1);
PruneFiles(myArgs.sPrunePath, myArgs);
}
/// <summary>
/// Prunes files based upon myArgs values
/// </summary>
/// <param name="myArgs">Configuration settings to use</param>
public static void PruneFiles(string sPrunePath, AutoPruneArgs myArgs)
{
// Process files
if (!sPrunePath.EndsWith(Convert.ToString(Path.DirectorySeparatorChar)))
sPrunePath+=Convert.ToString(Path.DirectorySeparatorChar);
foreach(string sFile in Directory.GetFiles(sPrunePath))
{
bool bSkip = false;
if (myArgs.aInclude.Count > 0)
{
foreach(Regex oIncReg in myArgs.aInclude)
{
if (!oIncReg.IsMatch(sFile))
{
bSkip = true;
break;
}
}
}
if (!bSkip && myArgs.aExclude.Count > 0)
{
foreach(Regex oExcReg in myArgs.aExclude)
{
if (oExcReg.IsMatch(sFile))
{
bSkip = true;
break;
}
}
}
if (!bSkip)
{ // Process the file or folder
DateTime dtFileTime = File.GetLastWriteTime(sFile);
if (File.GetCreationTime(sFile) > dtFileTime)
dtFileTime = File.GetCreationTime(sFile);
if (dtFileTime <= myArgs.dtOlderThan)
{
if (myArgs.bVerbose)
Console.WriteLine("{0}: {1}", myArgs.eMethod, sFile);
switch (myArgs.eMethod)
{
case PruneMethod.Delete:
Delete(sFile);
break;
case PruneMethod.Recycle:
Recycle(sFile);
break;
case PruneMethod.Move:
string sMoveToPath = Path.GetDirectoryName(sFile);
if (!sMoveToPath.EndsWith(
Convert.ToString(Path.DirectorySeparatorChar)))
sMoveToPath+=Path.DirectorySeparatorChar;
sMoveToPath = myArgs.sMovePath
+ sMoveToPath.Substring(myArgs.sPrunePath.Length)
+ Path.GetFileName(sFile);
Move(sFile,sMoveToPath);
break;
}
}
}
}
// Proces subdirs
foreach(string sDir in Directory.GetDirectories(sPrunePath))
{
bool bSkip = false;
if (myArgs.aInclude.Count > 0)
{
foreach(Regex oIncReg in myArgs.aInclude)
{
if (!oIncReg.IsMatch(sDir))
{
bSkip = true;
break;
}
}
}
if (!bSkip && myArgs.aExclude.Count > 0)
{
foreach(Regex oExcReg in myArgs.aExclude)
{
if (oExcReg.IsMatch(sDir))
{
bSkip = true;
break;
}
}
}
if (!bSkip)
{ // Process this subdirs files and folders
PruneFiles(sDir,myArgs);
// Check if this folder should be deleted
if (myArgs.bRemoveEmptyDirs && String.Compare(sDir,myArgs.sPrunePath) != 0)
{
try
{
Directory.Delete(sDir);
if (myArgs.bVerbose)
Console.WriteLine("Deleting directory: " + sDir);
}
catch (IOException)
{
// Directory is not empty
}
}
}
}
}
// Contains information that the SHFileOperation function uses to perform file operations.
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct SHFILEOPSTRUCT
{
public IntPtr hwnd;
public UInt32 wFunc;
public IntPtr pFrom;
public IntPtr pTo;
public UInt16 fFlags;
public Int32 fAnyOperationsAborted;
public IntPtr hNameMappings;
[MarshalAs(UnmanagedType.LPWStr)]
public String lpszProgressTitle;
}
[DllImport("shell32.dll" , CharSet = CharSet.Unicode)]
public static extern Int32 SHFileOperation(ref SHFILEOPSTRUCT lpFileOp);
const int FO_DELETE = 3;
const int FOF_ALLOWUNDO = 0x40;
const int FOF_NOCONFIRMATION = 0x10;
/// <summary>
/// Sends the specified file/folder to the recycle bin
/// </summary>
/// <param name="sPath">File/folder to recycle</param>
public static void Recycle(string sPath)
{
SHFILEOPSTRUCT shf = new SHFILEOPSTRUCT();
shf.hwnd = IntPtr.Zero;
shf.wFunc = (uint) FO_DELETE;
shf.fFlags = (ushort) FOF_ALLOWUNDO | FOF_NOCONFIRMATION;
shf.pTo = Marshal.StringToHGlobalUni(sPath);
shf.fAnyOperationsAborted = 0;
shf.hNameMappings = IntPtr.Zero;
SHFileOperation(ref shf);
}
/// <summary>
/// Moves the specified file to the path, creating all directories
/// in sPath as necessary
/// </summary>
/// <param name="sSrc">File to move</param>
/// <param name="sDest">Move to</param>
public static void Move(string sSrc,string sDest)
{
if (!Directory.Exists(Path.GetDirectoryName(sDest)))
Directory.CreateDirectory(Path.GetDirectoryName(sDest));
if (File.Exists(sDest))
Delete(sDest);
File.Move(sSrc,sDest);
}
/// <summary>
/// Deletes a file/directory ignoring leaving read-only files/dirs alone
/// </summary>
/// <param name="sPath">File/folder to delete</param>
public static void Delete(string sPath)
{
// Determing source type
if (Directory.Exists(sPath)) // Directory
Directory.Delete(sPath,true);
else if(File.Exists(sPath))
{ // File
if (File.Exists(sPath)
&& ((File.GetAttributes(sPath) & FileAttributes.ReadOnly)
!= FileAttributes.ReadOnly))
File.Delete(sPath);
}
}
/// <summary>
/// Shows command-line usage
/// </summary>
public static void ShowUsage()
{
Console.Write(Environment.NewLine
+ "{0} - v{1}" + Environment.NewLine
+ "{2} - {3}" + Environment.NewLine
+ "Usage:" + Environment.NewLine
+ "{0} /older:datetime /prune:path [/recycle|/delete|/move:path] "
+ "[/include:...] [/exclude:...] [/leaveemptydirs] [/verbose]" + Environment.NewLine
+ "Where:" + Environment.NewLine
+ "\t/older:date\tSelects files older than the follow offset from now:"
+ Environment.NewLine
+ "\t\t\t[DAYS:HOURS:MINUTES]" + Environment.NewLine
+ "\t/prune:path\tPath to recursively prune files that have a last "
+ Environment.NewLine + "\t\t\tmodified older than older" + Environment.NewLine
+ "\t/recycle\tFiles are moved to the recycle bin" + Environment.NewLine
+ "\t/delete\t\tImmediately delets the file" + Environment.NewLine
+ "\t/move:path\tMoves the files to the specified path"
+ Environment.NewLine
+ "\t/include:...\tInclude files/directories based upon the specified "
+ Environment.NewLine + "\t\t\tregular expressions" + Environment.NewLine
+ "\t/exclude:...\tExlude files/directories based upon the specified "
+ Environment.NewLine + "\t\t\tregular expressions" + Environment.NewLine
+ "\t/leaveemptydirs\tDoes not remove empty directories" + Environment.NewLine
+ "\t/verbose\tWrites verbose information to the console" + Environment.NewLine
,System.Diagnostics.FileVersionInfo.GetVersionInfo(
System.Reflection.Assembly.GetExecutingAssembly().Location).FileDescription
,System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()
,System.Diagnostics.FileVersionInfo.GetVersionInfo(
System.Reflection.Assembly.GetExecutingAssembly().Location).LegalCopyright
,System.Diagnostics.FileVersionInfo.GetVersionInfo(
System.Reflection.Assembly.GetExecutingAssembly().Location).CompanyName);
}
/// <summary>
/// Parses the command line
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
public static bool ParseCommandline(string[] args, ref AutoPruneArgs myArgs)
{
if (args.Length == 0)
{
ShowUsage();
return false;
}
if (Regex.IsMatch(Environment.CommandLine,@"\s*(\/help|\/\?)\s*"
,RegexOptions.IgnoreCase))
{
ShowUsage();
return false;
}
if (Regex.IsMatch(Environment.CommandLine,@"(\/leaveemptydirs)",RegexOptions.IgnoreCase))
myArgs.bRemoveEmptyDirs = false;
if (Regex.IsMatch(Environment.CommandLine,@"(\/v|\/verbose)",RegexOptions.IgnoreCase))
myArgs.bVerbose = true;
// Older than
Match oMatch = Regex.Match(Environment.CommandLine
,@"\/older:\s*(?<days>\d+)(:(?<hrs>\d+))?(:(?<min>\d+))?"
,RegexOptions.IgnoreCase);
if (!oMatch.Success)
{
Console.Error.WriteLine("Older argument invalid or unspecified");
return false;
}
if (IsInt(oMatch.Groups["days"].ToString()))
myArgs.dtOlderThan = myArgs.dtOlderThan.AddDays(
-Convert.ToInt32(oMatch.Groups["days"].ToString()));
if (IsInt(oMatch.Groups["hrs"].ToString()))
myArgs.dtOlderThan = myArgs.dtOlderThan.AddHours(
-Convert.ToInt32(oMatch.Groups["hrs"].ToString()));
if (IsInt(oMatch.Groups["min"].ToString()))
myArgs.dtOlderThan = myArgs.dtOlderThan.AddMinutes(
-Convert.ToInt32(oMatch.Groups["min"].ToString()));
// Prune path
oMatch = Regex.Match(Environment.CommandLine
,@"\/prune:\s*(""(?<path>[^""]+)""|(?<path>\S+))"
,RegexOptions.IgnoreCase);
if (!oMatch.Success)
{
Console.Error.WriteLine("Prune path not specified");
return false;
}
myArgs.sPrunePath = oMatch.Groups["path"].ToString();
// Validate path
if (!Directory.Exists(myArgs.sPrunePath))
{
Console.Error.WriteLine("The prune path: {0} does not exist."
,myArgs.sPrunePath);
return false;
}
if (!myArgs.sPrunePath.EndsWith(Convert.ToString(Path.DirectorySeparatorChar)))
myArgs.sPrunePath+=Convert.ToString(Path.DirectorySeparatorChar);
myArgs.sPrunePath = Path.GetFullPath(myArgs.sPrunePath); // Get absolute path
// Method
if (Regex.IsMatch(Environment.CommandLine,@"\/recycle",RegexOptions.IgnoreCase))
myArgs.eMethod = PruneMethod.Recycle;
if (Regex.IsMatch(Environment.CommandLine,@"\/delete",RegexOptions.IgnoreCase))
myArgs.eMethod = PruneMethod.Delete;
oMatch = Regex.Match(Environment.CommandLine
,@"\/move:\s*(""(?<path>[^""]+)""|(?<path>\S+))"
,RegexOptions.IgnoreCase);
if (oMatch.Success)
{
myArgs.eMethod = PruneMethod.Move;
myArgs.sMovePath = oMatch.Groups["path"].ToString();
// Validate path
if (!Directory.Exists(myArgs.sMovePath))
{
Console.Error.WriteLine("The move path: {0} does not exist."
,myArgs.sMovePath);
return false;
}
if (!myArgs.sMovePath.EndsWith(Convert.ToString(Path.DirectorySeparatorChar)))
myArgs.sMovePath+=Convert.ToString(Path.DirectorySeparatorChar);
}
if (myArgs.eMethod == PruneMethod.None)
{
Console.Error.WriteLine(@"Please specify /recycle /delete or /move");
return false;
}
// Includes
oMatch = Regex.Match(Environment.CommandLine
,@"\/include:\s*(""(?<inc>[^""]+)""|(?<inc>[^,\s]+))"
+ @"(\s*,\s*(""(?<inc>[^""]+)""|(?<inc>[^,\s]+)))*"
,RegexOptions.IgnoreCase);
if (oMatch.Success)
{
foreach(Capture oIncRegEx in oMatch.Groups["inc"].Captures)
{
if (String.Compare(oIncRegEx.ToString(),",") != 0)
{
try
{
myArgs.aInclude.Add(new Regex(oIncRegEx.ToString()
,RegexOptions.IgnoreCase));
}
catch (ArgumentException)
{
Console.Error.WriteLine("Error parsing include expression: {0}"
,oIncRegEx.ToString());
return false;
}
}
}
}
// Excludes
oMatch = Regex.Match(Environment.CommandLine
,@"\/exclude:\s*(""(?<exc>[^""]+)""|(?<exc>[^,\s]+))"
+ @"(\s*,\s*(""(?<exc>[^""]+)""|(?<exc>[^,\s]+)))*"
,RegexOptions.IgnoreCase);
if (oMatch.Success)
{
foreach(Capture oExcRegEx in oMatch.Groups["exc"].Captures)
{
try
{
myArgs.aExclude.Add(new Regex(oExcRegEx.ToString()
,RegexOptions.IgnoreCase));
}
catch (ArgumentException)
{
Console.Error.WriteLine("Error parsing exclude expression: {0}"
,oExcRegEx.ToString());
return false;
}
}
}
if (myArgs.bVerbose)
{
Console.WriteLine( "Older than: {0}" + Environment.NewLine +
"Prune path: {1}" + Environment.NewLine +
"Method: {2}"
,myArgs.dtOlderThan
,myArgs.sPrunePath
,myArgs.eMethod);
if (myArgs.eMethod == PruneMethod.Move)
Console.WriteLine("Move path: {0}", myArgs.sMovePath);
if (myArgs.aInclude.Count == 0)
Console.WriteLine("Include: All files and folders");
else
{
Console.Write("Include: ");
for (int i = 0; i < myArgs.aInclude.Count; i++)
{
if (i > 0)
Console.Write(", ");
Console.Write(myArgs.aInclude[i].ToString());
}
Console.Write(Environment.NewLine);
}
if (myArgs.aExclude.Count == 0)
Console.WriteLine("Exclude: None");
else
{
Console.Write("Exclude: ");
for (int i = 0; i < myArgs.aExclude.Count; i++)
{
if (i > 0)
Console.Write(", ");
Console.Write(myArgs.aExclude[i].ToString());
}
Console.Write(Environment.NewLine);
}
}
return true;
}
/// <summary>
/// Returns true/false if the specified object is an integer
/// </summary>
/// <param name="obj">Object to check</param>
/// <returns>True/false</returns>
public static bool IsInt(object obj)
{
try
{
Convert.ToInt32(obj);
}
catch (FormatException)
{
return false;
}
catch (OverflowException)
{
return false;
}
return true;
}
}
}
| Attachment | Size |
|---|---|
| AutoPruneSrc.zip | 8.77 KB |
| AutoPrune.zip | 7.25 KB |
