Manage their size with CleanDir.js
Log file management can be a difficult job for system administrators because not all applications include built-in features for managing their log sizes and numbers. For example, Microsoft IIS provides the ability to log HTTP and SMTP traffic but doesn't provide any built-in tools for monitoring the amount of disk space the logs use. On my servers, I periodically use Windows Explorer to sort my files by date and delete old log files. Microsoft's BadmailAdmin.wsf script is also useful for managing the size of the Badmail folder on Microsoft Exchange and IIS SMTP servers. However, I wanted a script that could manage all the log file folders on my servers, so I wrote a script called CleanDir.js to do just that.
CleanDir.js limits the size of a folder by deleting files based on the criterion you specify. You can have the script delete files older than a specified number of days (in which case there's no maximum folder size). Alternatively, you can have the script delete the oldest files to keep the folder under a specified number of megabytes (in which case, there's no age limit to the files).
To run CleanDir.js, you need Windows XP or later or Windows 2000 with Windows Script Host (WSH) 5.6, which is already installed on your PC if you've installed Microsoft Internet Explorer (IE) 6.0. The following is the command-line syntax for CleanDir.js:
\[cscript\] cleandir.js path \[...\] /d:days | /z:size \[/l\[:\[+\]\[logfile\]\]\] \[/s\] \[/t\]
The path parameter specifies the name of the folder containing the files that you want to delete. (Note that you must enclose the folder's name in quotation marks if it contains spaces.) You can specify multiple folder names on the script's command line.
The cscript keyword at the beginning of the command line is necessary only if CScript isn't your default script host. You can enter the following command at the command prompt to set CScript as your default script host:
cscript //h:cscript //nologo //s
To delete files older than a certain number of days, use the /d parameter with the number of days as its argument. For example, /d:30 will delete all files older than 30 days. To limit a folder's size, use the /z parameter with a number of megabytes as its argument. For example, /z:650 will limit the folder size to 650MB. You must specify either /d or /z. Note that the oldest files are always deleted first, no matter which parameter you choose.
To save the script's activity to a log file, use the /l parameter followed by the log file's name. If you don't specify a name, the script will name the file CleanDir.log and put it in the same folder as the script. Typically, the script will overwrite existing log files. However, you can prefix the log file's name with the plus (+) character to make the script append to the log file instead. For example, /l:Cleanup.log logs to the Cleanup.log file (overwriting it if it already exists), whereas /l:+Cleanup.log appends to Cleanup.log instead of overwriting it. If you don't specify the /l parameter, the script will write its output to the console window.
Figure 1 shows an example of what the logged information looks like. In the first column of each line, CleanDir.js uses the right angle bracket (>) to indicate informational messages and the exclamation point (!) to indicate errors. Two spaces precede each deleted file.
The /s parameter instructs CleanDir.js to process the subfolders of each named folder. If you use /z and /s together, the /z setting will be applied to each individual folder that the script processes. For example, if you use the command
cleandir.js c:\logs /z:650 /s
the script will limit the size of the individual files (not including subfolders and their contents) in the C:\logs folder to 650MB and will limit the size of each subfolder in C:\logs to 650MB. It doesn't limit the size of C:\logs and its subfolders combined because each folder is processed separately.
The /t parameter runs CleanDir.js in test mode (i.e., the script lists the files it would delete but doesn't actually delete any files). The last line of the script's output (or of the log file, if logging is enabled) will indicate whether test mode was enabled. Because the script is designed to delete files, I strongly recommend testing it thoroughly with the /t parameter before using the script in production.
After you've determined that the script works correctly, you can use Microsoft Task Scheduler to run it regularly. If the account that you're using to execute the task doesn't have CScript set as its default script host, I recommend running the program cscript.exe followed by the script, as the following command shows:
cscript.exe C:\Scripts\CleanDir.js C:\Inetpub\Logs\SMTPSVC1 /d:30 /l
This command executes C:\Scripts\CleanDir.js, which cleans out the C:\Inetpub\Logs\SMTPSVC1 folder. Files older than 30 days will be deleted, and the script will log its activity to the C:\Scripts\CleanDir.log file. You'll need to prefix the script's filename with the appropriate path if the script doesn't exist in the C:\Scripts folder.
CleanDir.js begins by declaring a set of global variables that perform different functions later in the script. Three of the variables, which Table 1 shows, let you customize the thousands, date, and time separators that the script uses in its log file. The SCRIPT_NAME variable is used in the usage function when displaying the script's name, and the numeric variables are used as constants later in the script.
Then, the main body of the script calls the Quit method of the WScript object, using the main function as its argument. In other words, the return value of the main function will be the script's exit code, making it easy to end the script from the main function. You just need to use a return statement with the desired exit code.
The main Function
Listing 1 shows the main function. (You can download the entire script by clicking the Download the Code Here button.) This function begins by declaring its own set of variables, then validates the script's command-line arguments. The command line must contain at least one unnamed argument—that is, an argument that doesn't start with a slash mark (\). If there aren't any unnamed arguments present or if the /? argument is present, the main function calls the usage function. The usage function displays the syntax for launching the script, then exits the script.
Next, the main function uses the scripthost function to determine which executable is running the script. If the script host isn't cscript.exe, the main function displays an error message and exits the script with a non-zero exit code.
The code at callout A in Listing 1 creates the options object, which contains the parameters that control the script's behavior. The script's command-line arguments determine some of the options object's properties. The options object contains two numeric properties, filetotal and bytetotal, which are updated by the deletefile function as files are deleted.
After creating the options object, the main function checks to see whether the /d parameter exists on the command line. If the /d parameter has an argument, the main function calls the parseInt function to assign the /d parameter's argument to the dayslimit property of the options object. If the parseInt function's result isn't numeric or is less than zero, the main function displays an error message and ends the script with a non-zero exit code. If the parseInt function's result is a positive number, the main function uses the datefromdaysago function to return a Date object representing the number of days ago and assigns it to the options object's cutoffdate property.
If the options object's dayslimit property is zero (i.e., the user didn't specify /d), the main function checks to see whether the /z parameter exists on the command line. If the /z parameter has an argument, the main function once again uses the parseInt function to assign the /z parameter's argument to the options object's sizelimit property. If the resulting value isn't numeric or is less than zero, the main function displays an error message and ends the script with a non-zero exit code.
When the options object's dayslimit and sizelimit properties are both zero (i.e., neither /d nor /z exists on the command line), the main function executes the usage function. When the dayslimit or sizelimit property has a number greater than zero, the main function sets the options object's recurse and testmode properties based on the presence of the /s and /t parameters. When the parameters exist on the command line, the corresponding object properties are set to true.
In the code at callout B, the main function checks to see whether the /l parameter exists on the command line. If the parameter exists, the main function sets the options object's logging property to true, indicating that logging is enabled. When logging is enabled, the function reads the /l parameter's argument (i.e., the log file's name). If the /l parameter's argument is a string of one or more characters, the main function determines whether the first character of the string is a +, which indicates that the log file should be appended to instead of overwritten. When the first character of the /l parameter's argument is a +, the remainder of the string is the log file's name. If the /l parameter's argument doesn't have a filename following the +, the main function assigns the default filename (CleanDir.log) to the options object's logfn property. Similarly, the function assigns the default filename to the logfn property when the /l parameter exists but doesn't have an argument.
Next, the main function encloses a call to the FileSystemObject object's OpenTextFile method in a try...catch…finally statement. In JScript, these statements are used to handle errors. (In VBScript, you'd use the On Error Resume Next statement instead.) The main function uses JScript's conditional ternary (three-part) operator to open the file in overwrite or append mode based on the logappend variable. If the main function fails to open the log file, it echoes an error message and sets the options object's logging property to false. By setting the logging property to false, the main function permits the script to continue running without creating a log file—the script's output will go to the console window instead. If the main function opened the log file successfully, the options object's logts property contains the TextStream object created by the OpenTextFile method.
After opening the log file, the main function creates an output string that contains one of the following informational messages:
- Operation: Delete files older than x day(s), where x is the number of days specified on the command line.
- Operation: Delete files to limit directory size(s) to y megabyte(s), where y is the size specified on the command line.
The main function reads the options object's dayslimit and sizelimit properties to determine which message it will use. The main function uses the formatnumber and formatdate functions to construct this string and calls the outputdata function to output the information.
At this point, the main function has parsed the command line and configured the options object's properties. The function's next task is to iterate through the command line's unnamed arguments by enumerating the WshUnnamed collection. Unlike VBScript's For Each…Next statement, JScript's for statement doesn't have built-in support for collections. Instead, you first have to create an Enumerator object that returns a reference to the collection. You then need to use the for statement and the Enumerator object's methods to iterate through the collection and read each item.
The main function reads each unnamed argument and attempts to assign the argument to a Folder object using the FileSystemObject object's GetFolder method. The function encloses the Folder object in a try...catch…finally statement in case the GetFolder method raises an error. If an error occurs, the code in the catch block will execute instead and output an error message before continuing on to the next element in the collection. If an error doesn't occur, the main function calls the process method with the Folder object and options object as parameters.
After the main function has iterated through all the unnamed command-line arguments and processed each folder, it creates an output string containing the options object's filetotal and bytetotal properties, outputs the string using the outputdata function, and closes the open log file if logging is enabled. Finally, the function returns an exit code of 0, which ends the script from the main function.
The formatnumber Function
Unlike VBScript, JScript doesn't have a built-in function for formatting numbers, so I wrote a function that inserts thousands separators in a number and returns the number as a string. To insert the thousands separators, the formatnumber function converts the number to a string using the toString method and executes the split method on the String object with a null string ("") as the split method's argument. Doing so creates an array in which each array element contains a single digit. The formatnumber function then starts at the last array element and uses the splice method to insert the THOUSANDS_SEPARATOR character at every third array position counting backward. Finally, the formatnumber function uses the array's join method to return the array as a single string representation of the number with thousands separators.
The datefromdaysago Function
The datefromdaysago function returns a Date object containing the date and time an exact number of days ago. Before I describe the function, however, I need to explain how a date and time are stored in a Date object. A Date object contains a date and time value as a number of milliseconds since January 1, 1970, Coordinated Universal Time (UTC). You can call a Date object's getTime method to return its millisecond representation, and you can set a Date object's date and time using milliseconds.
I took advantage of these two facts when I created the datefromdaysago function. This function works by creating a temporary Date object containing the current date and time and calls its getTime method, which returns the temporary Date object's millisecond representation. Then, the datefromdaysago function multiplies the number of days by the number of milliseconds in a day (i.e., 86,400,000), subtracts that number from the temporary Date object's millisecond value, and creates a new Date object with this millisecond value. Finally, the datefromdaysago function uses the resulting Date object as its return value.
The formatdate Function
I wrote the formatdate function because although JScript includes several methods that convert Date objects into strings, the resulting strings' format isn't as concise as I needed. The formatdate function uses the passed Date object's methods to return the various parts of the date and time in a string that follows the format: yyyy-mm-dd hh:mm:ss. The hyphens between the date components can be changed to a different character by editing the DATE_SEPARATOR character, and the colons between the time components can be changed by editing the TIME_SEPARATOR character. For example, some countries use a period (.) as a thousands separator, whereas other countries use a comma (,) as a decimal separator.
The outputdata Function
The outputdata function has two parameters: str and options. The outputdata function uses the options object to determine whether logging is enabled (i.e., the /l parameter exists on the command line, and the main function successfully opened the log file). If logging is enabled, the outputdata function writes the output string (represented by the str parameter) to the log file by calling the TextStream object's WriteLine method. (The options object's logts property is a TextStream object.) If the WriteLine method fails, the outputdata function uses WScript.Echo to send the script's output to the console window, even if the function fails to write the data to the log file. If logging isn't enabled, the outputdata function uses WScript.Echo to write the output string to the console window.
The process Function
The process function has two parameters: the Folder object and the options object. As I described earlier, the main function enumerates the script's unnamed command-line arguments by referencing the WshUnnamed collection and assigning each element in the collection to a Folder object, which then gets passed to the process function.
First, the process function declares its own set of variables. Next, it determines whether the options object's recurse property is true. If the property is true, the function creates an Enumerator object to enumerate the SubFolders collection for the current Folder object. The Enumerator object enumerates the collection of subfolders and passes each Folder object to the process function. The process function then calls the outputdata function to display an informational message containing the current folder's name and creates an empty Array object to contain an array of File objects.
After creating the array, the process function creates another Enumerator object to iterate through the Files collection in the current Folder object. Then, the process function adds each File object from the Files collection to the files array using the array's push method. The function keeps track of the cumulative sizes of the files in the Folder object by reading each File object's Size property.
Next, the process function calls the sort method to sort the array. In JScript, you can pass the name of an ordering function to the sort method, which then determines the sort order. The process function uses the sortbydate function as the sort method, which orders the array's File objects in ascending order by each File object's DateLastModified property (i.e., oldest to most recent).
At this point, the process function begins to determine which files it should delete. If the options object's dayslimit property is a non-zero—that is, if the /d parameter exists on the command line—the function iterates the array of File objects and checks each File object's DateLastModified property. If the DateLastModified property is less than the cutoff date (i.e., the options object's cutoffdate property), the function calls the deletefile function to delete the file.
If the options object's sizelimit property is a non-zero, it means that the /z parameter exists on the command line. In this case, the process function calculates the megabyte limit by multiplying the options object's sizelimit property by 1,048,576 (i.e., 1MB). Then, the process function uses a while statement to determine whether the folder is under the cutoff size. If the folder's size minus the size of deleted files is greater than the cutoff size, the while loop continues to execute.
Here’s how the while loop works. First, it determines the size of the current file to be deleted. Next, the while loop calls the deletefile function to attempt to delete the file. If the deletion is successful (i.e., if the deletefile function returns a zero), the while loop increments the deletion size total. Finally, the while loop determines whether it has reached the end of the array. If it has, the while loop uses the break statement to exit the while loop; otherwise, it increments the counter variable and continues on to the next File object in the array.
The deletefile Function
The deletefile function uses two parameters: the File object to be deleted and the options object. First, the deletefile function attempts to delete the File object by calling its Delete method. (The Delete method call is enclosed in a try…catch…finally statement in case it fails.) If the deletion is successful, the deletefile function updates the options object's bytetotal and filetotal properties, then uses the outputdata function to output the filename. If the deletion fails, the code in the catch block executes, and the deletefile function uses the outputdata function to output an error message.
Keeping Log File Directories Under Control
Many applications generate log files, however, not all of them provide tools for monitoring the amount of disk space they use. Use CleanDir.js to maintain the size and number of your log file folders.