-
Niko Ehrenfeuchter authoredNiko Ehrenfeuchter authored
AutoTx.cs 42.14 KiB
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.IO;
using System.Timers;
using System.DirectoryServices.AccountManagement;
using System.Globalization;
using System.Management;
using AutoTx.XmlWrapper;
using RoboSharp;
namespace AutoTx
{
public partial class AutoTx : ServiceBase
{
#region global variables
// naming convention: variables ending with "Path" are strings, variables
// ending with "Dir" are DirectoryInfo objects
private string _configPath;
private string _statusPath;
private string _incomingPath;
private string _managedPath;
private string[] _remoteUserDirs;
private string[] _localUserDirs;
private List<string> _transferredFiles = new List<string>();
private int _txProgress;
private const int MegaBytes = 1024 * 1024;
private const int GigaBytes = 1024 * 1024 * 1024;
private DateTime _lastUserDirCheck = DateTime.Now;
// the transfer state:
private enum TxState
{
Stopped = 0,
// Stopped: the last transfer was finished successfully or none was started yet.
// A new transfer may only be started if the service is in this state.
Active = 1,
// Active: a transfer is currently running (i.e. no new transfer may be started).
Paused = 2,
// Paused is assigned in PauseTransfer() which gets called from within RunMainTasks()
// when system parameters are not in their valid range. It gets evaluated if the
// parameters return to valid or if no user is logged on any more.
DoNothing = 3
// DoNothing is assigned when the service gets shut down (in the OnStop() method)
// to prevent accidentially launching new transfers etc.
}
private TxState _transferState;
private ServiceConfig _config;
private ServiceStatus _status;
private static Timer _mainTimer;
#endregion
#region initialize, load and check configuration + status
public AutoTx() {
InitializeComponent();
CreateEventLog();
LoadSettings();
CreateIncomingDirectories();
}
/// <summary>
/// Create the event log if it doesn't exist yet.
/// </summary>
private void CreateEventLog() {
try {
if (!EventLog.SourceExists(ServiceName)) {
EventLog.CreateEventSource(
ServiceName + "Log", ServiceName);
}
eventLog.Source = ServiceName + "Log";
eventLog.Log = ServiceName;
}
catch (Exception ex) {
writeLog("Error in createEventLog(): " + ex.Message, true);
}
}
/// <summary>
/// Load the initial settings.
/// </summary>
private void LoadSettings() {
try {
_transferState = TxState.Stopped;
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
_logPath = Path.Combine(baseDir, "service.log");
_configPath = Path.Combine(baseDir, "configuration.xml");
_statusPath = Path.Combine(baseDir, "status.xml");
LoadConfigXml();
LoadStatusXml();
_roboCommand = new RoboCommand();
}
catch (Exception ex) {
writeLog("Error in LoadSettings(): " + ex.Message + "\n" +
ex.StackTrace, true);
throw new Exception("Error in LoadSettings.");
}
// NOTE: this is explicitly called *outside* the try-catch block so an Exception
// thrown by the checker will not be silenced but cause the startup to fail:
CheckConfiguration();
}
/// <summary>
/// Load the configuration xml file.
/// </summary>
private void LoadConfigXml() {
try {
_config = ServiceConfig.Deserialize(_configPath);
_incomingPath = Path.Combine(_config.SourceDrive, _config.IncomingDirectory);
_managedPath = Path.Combine(_config.SourceDrive, _config.ManagedDirectory);
writeLogDebug("Loaded config from " + _configPath);
}
catch (ConfigurationErrorsException ex) {
writeLog("ERROR validating configuration file [" + _configPath +
"]: " + ex.Message);
throw new Exception("Error validating configuration.");
}
catch (Exception ex) {
writeLog("Error loading configuration XML: " + ex.Message, true);
// this should terminate the service process:
throw new Exception("Error loading config.");
}
}
/// <summary>
/// Load the status xml file.
/// </summary>
private void LoadStatusXml() {
try {
writeLogDebug("Trying to load status from " + _statusPath);
_status = ServiceStatus.Deserialize(_statusPath, _config);
writeLogDebug("Loaded status from " + _statusPath);
}
catch (Exception ex) {
writeLog("Error loading status XML from [" + _statusPath + "]: "
+ ex.Message + "\n" + ex.StackTrace, true);
// this should terminate the service process:
throw new Exception("Error loading status.");
}
}
/// <summary>
/// Check if loaded configuration is valid, print a summary to the log.
/// </summary>
public void CheckConfiguration() {
var configInvalid = false;
if (CheckSpoolingDirectories() == false) {
writeLog("ERROR checking spooling directories (incoming / managed)!");
configInvalid = true;
}
// terminate the service process if necessary:
if (configInvalid) throw new Exception("Invalid config, check log file!");
// check the clean-shutdown status and send a notification if it was not true,
// then set it to false while the service is running until it is properly
// shut down via the OnStop() method:
if (_status.CleanShutdown == false) {
writeLog("WARNING: " + ServiceName + " was not shut down properly last time!\n\n" +
"This could indicate the computer has crashed or was forcefully shut off.", true);
}
_status.CleanShutdown = false;
StartupSummary();
}
/// <summary>
/// Write a summary of loaded config + status to the log.
/// </summary>
private void StartupSummary() {
var msg = "Startup Summary:\n\n------ RoboSharp ------\n";
var roboDll = System.Reflection.Assembly.GetAssembly(typeof(RoboCommand)).Location;
if (roboDll != null) {
var versionInfo = FileVersionInfo.GetVersionInfo(roboDll);
msg += " > DLL file: " + roboDll + "\n" +
" > DLL description: " + versionInfo.Comments + "\n" +
" > DLL version: " + versionInfo.FileVersion + "\n";
}
msg += "\n------ Loaded status flags ------\n" + _status.Summary() +
"\n------ Loaded configuration settings ------\n" + _config.Summary();
msg += "\n------ Current system parameters ------\n" +
"Hostname: " + Environment.MachineName + "\n" +
"Free system memory: " + GetFreeMemory() + " MB" + "\n";
foreach (var driveToCheck in _config.SpaceMonitoring) {
msg += "Free space on drive '" + driveToCheck.DriveName + "': " +
GetFreeDriveSpace(driveToCheck.DriveName) + "\n";
}
msg += "\n------ Grace location status ------\n";
try {
msg += GraceLocationSummary();
}
catch (Exception ex) {
writeLog("CheckGraceLocation() failed: " + ex.Message, true);
}
if (!string.IsNullOrEmpty(_config.ValidationWarnings)) {
writeLog("WARNING: some configuration settings might not be optimal:\n" +
_config.ValidationWarnings);
}
if (!string.IsNullOrEmpty(_status.ValidationWarnings)) {
writeLog("WARNING: some status parameters were invalid and have been reset:\n" +
_status.ValidationWarnings);
}
writeLogDebug(msg);
}
#endregion
#region overrides for ServiceBase methods (start, stop, ...)
/// <summary>
/// Is executed when the service starts
/// </summary>
protected override void OnStart(string[] args) {
try {
_mainTimer = new Timer(_config.ServiceTimer);
_mainTimer.Elapsed += OnTimedEvent;
_mainTimer.Enabled = true;
}
catch (Exception ex) {
writeLog("Error in OnStart(): " + ex.Message, true);
}
// read the build timestamp from the resources:
var buildTimestamp = Properties.Resources.BuildDate.Trim();
writeLog("-----------------------");
writeLog(ServiceName + " service started <build " + buildTimestamp + ">");
writeLog("-----------------------");
}
/// <summary>
/// Executes when a Stop command is sent to the service by the Service Control Manager.
/// NOTE: the method is NOT triggered when the operating system shuts down, instead
/// the OnShutdown() method is used!
/// </summary>
protected override void OnStop() {
writeLog(ServiceName + " service stop requested...");
if (_transferState != TxState.Stopped) {
_transferState = TxState.DoNothing;
// Stop() is calling Process.Kill() (immediately forcing a termination of the
// process, returning asynchronously), followed by Process.Dispose()
// (releasing all resources used by the component). Would be nice if RoboSharp
// implemented a method to check if the process has actually terminated, but
// this is probably something we have to do ourselves.
try {
_roboCommand.Stop();
}
catch (Exception ex) {
writeLog("Error terminating the RoboCopy process: " + ex.Message, true);
}
_status.TransferInProgress = true;
writeLog("Not all files were transferred - will resume upon next start");
writeLogDebug("CurrentTransferSrc: " + _status.CurrentTransferSrc);
// should we delete an incompletely transferred file on the target?
SendTransferInterruptedMail();
}
// set the shutdown status to clean:
_status.CleanShutdown = true;
writeLog("-----------------------");
writeLog(ServiceName + " service stopped");
writeLog("-----------------------");
}
/// <summary>
/// Executes when the operating system is shutting down. Unlike some documentation says
/// it doesn't call the OnStop() method, so we have to do this explicitly.
/// </summary>
protected override void OnShutdown() {
writeLog("System is shutting down, requesting the service to stop.");
OnStop();
}
/// <summary>
/// Is executed when the service continues
/// </summary>
protected override void OnContinue() {
writeLog("-------------------------");
writeLog(ServiceName + " service resuming");
writeLog("-------------------------");
}
/// <summary>
/// Timer Event
/// </summary>
private void OnTimedEvent(object source, ElapsedEventArgs e) {
if (_transferState == TxState.DoNothing) return;
// first disable the timer event to prevent another one from being triggered
// while this method has not finished yet:
_mainTimer.Enabled = false;
try {
RunMainTasks();
GC.Collect();
}
catch (Exception ex) {
writeLog("Error in OnTimedEvent(): " + ex.Message, true);
writeLogDebug("Extended Error Info (StackTrace): " + ex.StackTrace);
}
finally {
// make sure to enable the timer again:
_mainTimer.Enabled = true;
}
}
#endregion
#region general methods
/// <summary>
/// Check system parameters for valid ranges and update the global service state accordingly.
/// </summary>
private void UpdateServiceState() {
var limitReason = "";
// check all system parameters for valid ranges and remember the reason in a string
// if one of them is failing (to report in the log why we're suspended)
if (GetCpuUsage() >= _config.MaxCpuUsage)
limitReason = "CPU usage";
else if (GetFreeMemory() < _config.MinAvailableMemory)
limitReason = "RAM usage";
else {
var blacklistedProcess = CheckForBlacklistedProcesses();
if (blacklistedProcess != "") {
limitReason = "blacklisted process '" + blacklistedProcess + "'";
}
}
// all parameters within valid ranges, so set the state to "Running":
if (string.IsNullOrEmpty(limitReason)) {
_status.ServiceSuspended = false;
if (!string.IsNullOrEmpty(_status.LimitReason)) {
_status.LimitReason = ""; // reset to force a message on next service suspend
writeLog("Service resuming operation (all parameters in valid ranges).");
}
return;
}
// set state to "Running" if no-one is logged on:
if (NoUserIsLoggedOn()) {
_status.ServiceSuspended = false;
if (!string.IsNullOrEmpty(_status.LimitReason)) {
_status.LimitReason = ""; // reset to force a message on next service suspend
writeLog("Service resuming operation (no user logged on).");
}
return;
}
// by reaching this point we know the service should be suspended:
_status.ServiceSuspended = true;
if (limitReason == _status.LimitReason)
return;
writeLog("Service suspended due to limitiations [" + limitReason + "].");
_status.LimitReason = limitReason;
}
/// <summary>
/// Do the main tasks of the service, check system state, trigger transfers, ...
/// </summary>
public void RunMainTasks() {
// mandatory tasks, run on every call:
CheckLogSize();
CheckFreeDiskSpace();
UpdateServiceState();
var delta = DateTime.Now - _lastUserDirCheck;
if (delta.Seconds >= 120)
CreateIncomingDirectories();
// tasks depending on the service state:
if (_status.ServiceSuspended) {
// make sure to pause any running transfer:
PauseTransfer();
} else {
// always check the incoming dirs, independently of running transfers:
CheckIncomingDirectories();
// now trigger potential transfer tasks:
RunTransferTasks();
}
}
/// <summary>
/// Helper method to create timestamp strings in a consistent fashion.
/// </summary>
/// <returns>A timestamp string of the current time.</returns>
private static string CreateTimestamp() {
return DateTime.Now.ToString("yyyy-MM-dd__HH-mm-ss");
}
#endregion
#region ActiveDirectory, email address, user name, ...
/// <summary>
/// Check if a user is currently logged into Windows.
///
/// WARNING: this DOES NOT ACCOUNT for users logged in via RDP!!
/// </summary>
/// See https://stackoverflow.com/questions/5218778/ for the RDP problem.
private bool NoUserIsLoggedOn() {
var username = "";
try {
var searcher = new ManagementObjectSearcher("SELECT UserName " +
"FROM Win32_ComputerSystem");
var collection = searcher.Get();
username = (string) collection.Cast<ManagementBaseObject>().First()["UserName"];
}
catch (Exception ex) {
writeLog("Error in getCurrentUsername(): " + ex.Message, true);
}
return username == "";
}
/// <summary>
/// Get the user email address from ActiveDirectory.
/// </summary>
/// <param name="username">The username.</param>
/// <returns>Email address of AD user, an empty string if not found.</returns>
public string GetEmailAddress(string username) {
try {
using (var pctx = new PrincipalContext(ContextType.Domain)) {
using (var up = UserPrincipal.FindByIdentity(pctx, username)) {
if (up != null && !String.IsNullOrEmpty(up.EmailAddress)) {
return up.EmailAddress;
}
}
}
}
catch (Exception ex) {
writeLog("Can't find email address for " + username + ": " + ex.Message);
}
return "";
}
/// <summary>
/// Get the full user name (human-friendly) from ActiveDirectory.
/// </summary>
/// <param name="username">The username.</param>
/// <returns>A human-friendly string representation of the user principal.</returns>
public string GetFullUserName(string username) {
try {
using (var pctx = new PrincipalContext(ContextType.Domain)) {
using (var up = UserPrincipal.FindByIdentity(pctx, username)) {
if (up != null) return up.GivenName + " " + up.Surname;
}
}
}
catch (Exception ex) {
writeLog("Can't find full name for " + username + ": " + ex.Message);
}
return "";
}
#endregion
#region transfer tasks
/// <summary>
/// Helper method to generate the full path of the current temp directory.
/// </summary>
/// <returns>A string with the path to the last tmp dir.</returns>
private string ExpandCurrentTargetTmp() {
return Path.Combine(_config.DestinationDirectory,
_config.TmpTransferDir,
_status.CurrentTargetTmp);
}
/// <summary>
/// Assemble the transfer destination path and check if it exists.
/// </summary>
/// <param name="dirName">The target directory to be checked on the destination.</param>
/// <returns>The full path if it exists, an empty string otherwise.</returns>
private string DestinationPath(string dirName) {
var destPath = Path.Combine(_config.DestinationDirectory, dirName);
if (Directory.Exists(destPath))
return destPath;
return "";
}
/// <summary>
/// Check for transfers to be finished, resumed or newly initiated.
/// </summary>
public void RunTransferTasks() {
// only proceed when in a valid state:
if (_transferState != TxState.Stopped &&
_transferState != TxState.Paused)
return;
// if we're paused, resume the transfer and DO NOTHING ELSE:
if (_transferState == TxState.Paused) {
ResumePausedTransfer();
return;
}
// first check if there are finished transfers and clean them up:
FinalizeTransfers();
// next check if there is a transfer that has to be resumed:
ResumeInterruptedTransfer();
// check the queueing location and dispatch new transfers:
ProcessQueuedDirectories();
}
/// <summary>
/// Process directories in the queueing location, dispatching new transfers if applicable.
/// </summary>
private void ProcessQueuedDirectories() {
// only proceed when in a valid state:
if (_transferState != TxState.Stopped)
return;
// check the "processing" location for directories:
var processingDir = Path.Combine(_managedPath, "PROCESSING");
var queued = new DirectoryInfo(processingDir).GetDirectories();
if (queued.Length == 0)
return;
var subdirs = queued[0].GetDirectories();
// having no subdirectories should not happen in theory - in practice it could e.g. if
// an admin is moving around stuff while the service is operating, so better be safe:
if (subdirs.Length == 0) {
writeLog("WARNING: empty processing directory found: " + queued[0].Name);
try {
queued[0].Delete();
writeLogDebug("Removed empty directory: " + queued[0].Name);
}
catch (Exception ex) {
writeLog("Error deleting directory: " + queued[0].Name + " - " + ex.Message);
}
return;
}
// dispatch the next directory from "processing" for transfer:
try {
StartTransfer(subdirs[0].FullName);
}
catch (Exception ex) {
writeLog("Error checking for data to be transferred: " + ex.Message);
throw;
}
}
/// <summary>
/// Check the incoming directories for files, move them to the processing location.
/// </summary>
private void CheckIncomingDirectories() {
// iterate over all user-subdirectories:
foreach (var userDir in new DirectoryInfo(_incomingPath).GetDirectories()) {
if (IncomingDirIsEmpty(userDir))
continue;
writeLog("Found new files in " + userDir.FullName);
MoveToManagedLocation(userDir);
}
}
/// <summary>
/// Check if a transfer needs to be completed by moving its data from the target temp dir
/// to the final (user) destination and by locally moving the transferred folders to the
/// grace location for deferred deletion.
/// </summary>
private void FinalizeTransfers() {
// NOTE: this is intentionally triggered by the timer only to make sure the cleanup
// only happens while all system parameters are within their valid ranges
// make sure the service is in an expected state before cleaning up:
if (_transferState != TxState.Stopped || _status.TransferInProgress)
return;
if (_status.CurrentTargetTmp.Length > 0) {
writeLogDebug("Finalizing transfer, cleaning up target storage location...");
var finalDst = DestinationPath(_status.CurrentTargetTmp);
if (!string.IsNullOrWhiteSpace(finalDst)) {
if (MoveAllSubDirs(new DirectoryInfo(ExpandCurrentTargetTmp()), finalDst, true)) {
_status.CurrentTargetTmp = "";
}
}
}
if (_status.CurrentTransferSrc.Length > 0) {
writeLogDebug("Finalizing transfer, moving local data to grace location...");
MoveToGraceLocation();
SendTransferCompletedMail();
_status.CurrentTransferSrc = ""; // cleanup completed, so reset CurrentTransferSrc
_status.CurrentTransferSize = 0;
_transferredFiles.Clear(); // empty the list of transferred files
}
}
/// <summary>
/// Check if an interrupted (service shutdown) transfer exists and whether the current
/// state allows for resuming it.
/// </summary>
private void ResumeInterruptedTransfer() {
// CONDITIONS (a transfer has to be resumed):
// - CurrentTargetTmp has to be non-empty
// - TransferState has to be "Stopped"
// - TransferInProgress must be true
if (_status.CurrentTargetTmp.Length <= 0 ||
_transferState != TxState.Stopped ||
_status.TransferInProgress == false)
return;
writeLogDebug("Resuming interrupted transfer from '" + _status.CurrentTransferSrc +
"' to '" + ExpandCurrentTargetTmp() + "'");
StartTransfer(_status.CurrentTransferSrc);
}
#endregion
#region filesystem tasks (check, move, ...)
/// <summary>
/// Check if a given directory is empty. If a marker file is set in the config a
/// file with this name will be created inside the given directory and will be
/// skipped itself when checking for files and directories.
/// </summary>
/// <param name="dirInfo">The directory to check.</param>
/// <returns>True if access is denied or the dir is empty, false otherwise.</returns>
private bool IncomingDirIsEmpty(DirectoryInfo dirInfo) {
try {
var filesInTree = dirInfo.GetFiles("*", SearchOption.AllDirectories);
if (string.IsNullOrEmpty(_config.MarkerFile))
return filesInTree.Length == 0;
// check if there is ONLY the marker file:
if (filesInTree.Length == 1 &&
filesInTree[0].Name.Equals(_config.MarkerFile))
return true;
// make sure the marker file is there:
var markerFilePath = Path.Combine(dirInfo.FullName, _config.MarkerFile);
if (! File.Exists(markerFilePath))
File.Create(markerFilePath);
return filesInTree.Length == 0;
}
catch (Exception e) {
writeLog("Error accessing directories: " + e.Message);
}
// if nothing triggered before, we pretend the dir is empty:
return true;
}
/// <summary>
/// Collect individual files in a user dir in a specific sub-directory. If a marker
/// file is set in the configuration, this will be skipped in the checks.
/// </summary>
/// <param name="userDir">The user directory to check for individual files.</param>
private void CollectOrphanedFiles(DirectoryInfo userDir) {
var fileList = userDir.GetFiles();
var orphanedDir = Path.Combine(userDir.FullName, "orphaned");
try {
if (fileList.Length > 1 ||
(string.IsNullOrEmpty(_config.MarkerFile) && fileList.Length > 0)) {
if (Directory.Exists(orphanedDir)) {
writeLog("Orphaned directory already exists, skipping individual files.");
return;
}
writeLogDebug("Found individual files, collecting them in 'orphaned' folder.");
CreateNewDirectory(orphanedDir, false);
}
foreach (var file in fileList) {
if (file.Name.Equals(_config.MarkerFile))
continue;
writeLogDebug("Collecting orphan: " + file.Name);
file.MoveTo(Path.Combine(orphanedDir, file.Name));
}
}
catch (Exception ex) {
writeLog("Error collecting orphaned files: " + ex.Message + ex.StackTrace);
}
}
/// <summary>
/// Check the incoming directory for files and directories, move them over
/// to the "processing" location (a sub-directory of ManagedDirectory).
/// </summary>
private void MoveToManagedLocation(DirectoryInfo userDir) {
string errMsg;
try {
// first check for individual files and collect them:
CollectOrphanedFiles(userDir);
// the default subdir inside the managed directory, where folders will be
// picked up later by the actual transfer method:
var target = "PROCESSING";
// if the user has no directory on the destination move to UNMATCHED instead:
if (string.IsNullOrWhiteSpace(DestinationPath(userDir.Name))) {
writeLog("Found unmatched incoming dir: " + userDir.Name, true);
target = "UNMATCHED";
}
// now everything that is supposed to be transferred is in a folder,
// for example: D:\ATX\PROCESSING\2017-04-02__12-34-56\user00
var targetDir = Path.Combine(
_managedPath,
target,
CreateTimestamp(),
userDir.Name);
if (MoveAllSubDirs(userDir, targetDir))
return;
errMsg = "unable to move " + userDir.FullName;
}
catch (Exception ex) {
errMsg = ex.Message;
}
writeLog("MoveToManagedLocation(" + userDir.FullName + ") failed: " + errMsg, true);
}
/// <summary>
/// Move transferred files to the grace location for deferred deletion. Data is placed in
/// a subdirectory with the current date and time as its name to denote the timepoint
/// when the grace period for this data starts.
/// </summary>
public void MoveToGraceLocation() {
string errMsg;
// CurrentTransferSrc is e.g. D:\ATX\PROCESSING\2017-04-02__12-34-56\user00
var sourceDirectory = new DirectoryInfo(_status.CurrentTransferSrc);
var dstPath = Path.Combine(
_managedPath,
"DONE",
sourceDirectory.Name, // the username directory
CreateTimestamp());
// writeLogDebug("MoveToGraceLocation: src(" + sourceDirectory.FullName + ") dst(" + dstPath + ")");
try {
if (MoveAllSubDirs(sourceDirectory, dstPath)) {
// clean up the processing location:
sourceDirectory.Delete();
if (sourceDirectory.Parent != null)
sourceDirectory.Parent.Delete();
// check age and size of existing folders in the grace location after
// a transfer has completed, trigger a notification if necessary:
CheckGraceLocation();
return;
}
errMsg = "unable to move " + sourceDirectory.FullName;
}
catch (Exception ex) {
errMsg = ex.Message;
}
writeLog("MoveToGraceLocation() failed: " + errMsg, true);
}
/// <summary>
/// Move all subdirectories of a given path into a destination directory. The destination
/// will be created if it doesn't exist yet. If a subdirectory of the same name already
/// exists in the destination, a timestamp-suffix is added to the new one.
/// </summary>
/// <param name="sourceDir">The source path as DirectoryInfo object.</param>
/// <param name="destPath">The destination path as a string.</param>
/// <returns>True on success, false otherwise.</returns>
private bool MoveAllSubDirs(DirectoryInfo sourceDir, string destPath, bool resetAcls = false) {
// TODO: check whether _transferState should be adjusted while moving dirs!
writeLogDebug("MoveAllSubDirs: " + sourceDir.FullName + " to " + destPath);
try {
// make sure the target directory that should hold all subdirectories to
// be moved is existing:
if (string.IsNullOrEmpty(CreateNewDirectory(destPath, false))) {
writeLog("WARNING: destination path doesn't exist: " + destPath);
return false;
}
foreach (var subDir in sourceDir.GetDirectories()) {
var target = Path.Combine(destPath, subDir.Name);
// make sure NOT to overwrite the subdirectories:
if (Directory.Exists(target))
target += "_" + CreateTimestamp();
writeLogDebug(" - " + subDir.Name + " > " + target);
subDir.MoveTo(target);
if (resetAcls && _config.EnforceInheritedACLs) {
try {
var acl = Directory.GetAccessControl(target);
acl.SetAccessRuleProtection(false, false);
Directory.SetAccessControl(target, acl);
writeLogDebug("Successfully reset inherited ACLs on " + target);
}
catch (Exception ex) {
writeLog("Error resetting inherited ACLs on " + target + ":\n" +
ex.Message);
}
}
}
}
catch (Exception ex) {
writeLog("Error moving directories: " + ex.Message + "\n" +
sourceDir.FullName + "\n" +
destPath, true);
return false;
}
return true;
}
/// <summary>
/// Create a directory with the given name if it doesn't exist yet, otherwise
/// (optionally) create a new one using a date suffix to distinguish it from
/// the existing one.
/// </summary>
/// <param name="dirPath">The full path of the directory to be created.</param>
/// <param name="unique">Add a time-suffix to the name if the directory exists.</param>
/// <returns>The name of the (created or pre-existing) directory. This will only
/// differ from the input parameter "dirPath" if the "unique" parameter is set
/// to true (then it will give the newly generated name) or if an error occured
/// (in which case it will return an empty string).</returns>
private string CreateNewDirectory(string dirPath, bool unique) {
try {
if (Directory.Exists(dirPath)) {
// if unique was not requested, return the name of the existing dir:
if (unique == false)
return dirPath;
dirPath = dirPath + "_" + CreateTimestamp();
}
Directory.CreateDirectory(dirPath);
writeLogDebug("Created directory: " + dirPath);
return dirPath;
}
catch (Exception ex) {
writeLog("Error in CreateNewDirectory(" + dirPath + "): " + ex.Message, true);
}
return "";
}
/// <summary>
/// Helper method to check if a directory exists, trying to create it if not.
/// </summary>
/// <param name="path">The full path of the directory to check / create.</param>
/// <returns>True if existing or creation was successful, false otherwise.</returns>
private bool CheckForDirectory(string path) {
if (string.IsNullOrWhiteSpace(path)) {
writeLog("ERROR: CheckForDirectory() parameter must not be empty!");
return false;
}
return CreateNewDirectory(path, false) == path;
}
/// <summary>
/// Ensure the required spooling directories (managed/incoming) exist.
/// </summary>
/// <returns>True if all dirs exist or were created successfully.</returns>
private bool CheckSpoolingDirectories() {
var retval = CheckForDirectory(_incomingPath);
retval &= CheckForDirectory(_managedPath);
retval &= CheckForDirectory(Path.Combine(_managedPath, "PROCESSING"));
retval &= CheckForDirectory(Path.Combine(_managedPath, "DONE"));
retval &= CheckForDirectory(Path.Combine(_managedPath, "UNMATCHED"));
return retval;
}
/// <summary>
/// Helper to create directories for all users that have one in the local
/// user directory (C:\Users) AND in the DestinationDirectory.
/// </summary>
private void CreateIncomingDirectories() {
_localUserDirs = new DirectoryInfo(@"C:\Users")
.GetDirectories()
.Select(d => d.Name)
.ToArray();
_remoteUserDirs = new DirectoryInfo(_config.DestinationDirectory)
.GetDirectories()
.Select(d => d.Name)
.ToArray();
foreach (var userDir in _localUserDirs) {
// don't create an incoming directory for the same name as the
// temporary transfer location:
if (_config.TmpTransferDir == userDir)
continue;
// don't create a directory if it doesn't exist on the target:
if (!_remoteUserDirs.Contains(userDir))
continue;
CreateNewDirectory(Path.Combine(_incomingPath, userDir), false);
}
_lastUserDirCheck = DateTime.Now;
}
/// <summary>
/// Recursively sum up size of all files under a given path.
/// </summary>
/// <param name="path">Full path of the directory.</param>
/// <returns>The total size in bytes.</returns>
public static long GetDirectorySize(string path) {
return new DirectoryInfo(path)
.GetFiles("*", SearchOption.AllDirectories)
.Sum(file => file.Length);
}
public void CheckGraceLocation() {
writeLogDebug(GraceLocationSummary());
}
/// <summary>
/// Generate a report on expired folders in the grace location.
///
/// Check all user-directories in the grace location for subdirectories whose
/// name-timestamp exceeds the configured grace period and generate a summary
/// containing the age and size of those directories.
/// </summary>
public string GraceLocationSummary() {
var expired = ExpiredDirs(_config.GracePeriod);
var report = "";
foreach (var userdir in expired.Keys) {
report += "\n - user '" + userdir + "'\n";
foreach (var subdir in expired[userdir]) {
report += string.Format(" - {0} [age: {2} days, size: {1} MB]\n",
subdir.Item1, subdir.Item2, subdir.Item3);
}
}
if (string.IsNullOrEmpty(report))
return "No expired folders in grace location.\n";
return "Expired folders in grace location:\n" + report;
}
/// <summary>
/// Assemble a dictionary with information about expired directories.
/// </summary>
/// <returns>A dictionary having usernames as keys (of those users that actually do have
/// expired directories), where the values are lists of tuples with the DirInfo objects,
/// size and age (in days) of the expired directories.</returns>
private Dictionary<string, List<Tuple<DirectoryInfo, long, int>>> ExpiredDirs(int thresh) {
var collection = new Dictionary<string, List<Tuple<DirectoryInfo, long, int>>>();
var graceDir = new DirectoryInfo(Path.Combine(_managedPath, "DONE"));
var now = DateTime.UtcNow;
foreach (var userdir in graceDir.GetDirectories()) {
var expired = new List<Tuple<DirectoryInfo, long, int>>();
foreach (var subdir in userdir.GetDirectories()) {
var age = DirNameToAge(subdir, now);
if (age < thresh)
continue;
long size = -1;
try {
size = GetDirectorySize(subdir.FullName);
}
catch (Exception ex) {
writeLog("ERROR getting directory size of " + subdir.FullName +
" - " + ex.Message);
}
expired.Add(new Tuple<DirectoryInfo, long, int>(subdir, size, age));
}
if (expired.Count > 0)
collection.Add(userdir.Name, expired);
}
return collection;
}
/// <summary>
/// Convert the timestamp given by the NAME of a directory into the age in days.
/// </summary>
/// <param name="dir">The DirectoryInfo object to check for its name-age.</param>
/// <param name="baseTime">The DateTime object to compare to.</param>
/// <returns>The age in days, or -1 in case of an error.</returns>
private int DirNameToAge(DirectoryInfo dir, DateTime baseTime) {
DateTime dirTimestamp;
try {
dirTimestamp = DateTime.ParseExact(dir.Name, "yyyy-MM-dd__HH-mm-ss",
CultureInfo.InvariantCulture);
}
catch (Exception ex) {
writeLogDebug("ERROR parsing timestamp from directory name '" +
dir.Name + "', skipping: " + ex.Message);
return -1;
}
return (baseTime - dirTimestamp).Days;
}
#endregion
}
}