Newer
Older
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;
Niko Ehrenfeuchter
committed
using AutoTx.XmlWrapper;
using RoboSharp;
namespace AutoTx
{
public partial class AutoTx : ServiceBase
{
#region global variables
Niko Ehrenfeuchter
committed
// 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;
Niko Ehrenfeuchter
committed
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();
}
/// Load the configuration xml file.
private void LoadConfigXml() {
Niko Ehrenfeuchter
committed
_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() {
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);
Niko Ehrenfeuchter
committed
msg += " > DLL file: " + roboDll + "\n" +
" > DLL description: " + versionInfo.Comments + "\n" +
" > DLL version: " + versionInfo.FileVersion + "\n";
Niko Ehrenfeuchter
committed
msg += "\n------ Loaded status flags ------\n" + _status.Summary() +
"\n------ Loaded configuration settings ------\n" + _config.Summary();
Niko Ehrenfeuchter
committed
msg += "\n------ Current system parameters ------\n" +
"Hostname: " + Environment.MachineName + "\n" +
"Free system memory: " + GetFreeMemory() + " MB" + "\n";
foreach (var driveToCheck in _config.SpaceMonitoring) {
Niko Ehrenfeuchter
committed
msg += "Free space on drive '" + driveToCheck.DriveName + "': " +
GetFreeDriveSpace(driveToCheck.DriveName) + "\n";
Niko Ehrenfeuchter
committed
msg += "\n------ Grace location status (threshold: " + _config.GracePeriod + ") ------\n";
var tmp = GraceLocationSummary(_config.GracePeriod);
if (string.IsNullOrEmpty(tmp)) {
msg += " -- NO EXPIRED folders in grace location! --\n";
} else {
msg += tmp;
}
}
catch (Exception ex) {
writeLog("GraceLocationSummary() 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();
var buildCommitName = Properties.Resources.BuildCommit.Trim();
writeLog("-----------------------");
writeLog(ServiceName + " service started.");
writeLog("commit: [" + buildCommitName + "]");
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?
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
}
// 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();
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
}
}
/// <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) {
return;
}
// first check if there are finished transfers and clean them up:
// 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:
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
Niko Ehrenfeuchter
committed
_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.
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)
writeLogDebug("Resuming interrupted transfer from '" + _status.CurrentTransferSrc +
"' to '" + ExpandCurrentTargetTmp() + "'");
StartTransfer(_status.CurrentTransferSrc);
#region filesystem tasks (check, move, ...)
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
/// <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,
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:
writeLogDebug(GraceLocationSummary(_config.GracePeriod));
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);
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);
}
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
}
}
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 "";
}
Niko Ehrenfeuchter
committed
/// <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;
}
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
/// <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;
}
Niko Ehrenfeuchter
committed
/// <summary>
/// Recursively sum up size of all files under a given path.
/// </summary>
/// <param name="path">Full path of the directory.</param>
Niko Ehrenfeuchter
committed
/// <returns>The total size in bytes.</returns>
public static long GetDirectorySize(string path) {
return new DirectoryInfo(path)
.GetFiles("*", SearchOption.AllDirectories)
.Sum(file => file.Length);
}
/// <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>
/// <param name="threshold">The number of days used as expiration threshold.</param>
public string GraceLocationSummary(int threshold) {
var expired = ExpiredDirs(threshold);
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 "";
report = "Expired folders in grace location:\n" + report;
return report;
}
/// <summary>
/// Assemble a dictionary with information about expired directories.
/// </summary>
/// <param name="thresh">The number of days used as expiration threshold.</param>
/// <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;
size = GetDirectorySize(subdir.FullName) / MegaBytes;
}
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) {