diff --git a/dotnet/ComparisonDifference.cs b/dotnet/ComparisonDifference.cs new file mode 100644 index 000000000..6793ec1fb --- /dev/null +++ b/dotnet/ComparisonDifference.cs @@ -0,0 +1,53 @@ +using System; +using System.Globalization; +using System.Runtime.InteropServices; +using WinSCP; + +namespace WinSCP +{ + [Guid("B1DAE3A0-5E56-4001-88D8-786F68557E28")] + [ComVisible(true)] + public enum SynchronizationAction + { + UploadNew = 1, + DownloadNew = 2, + UploadUpdate = 3, + DownloadUpdate = 4, + DeleteRemote = 5, + DeleteLocal = 6, + }; + + [Guid("2D6EFFB5-69BA-47AA-90E8-A92953E8B58A")] + [ComVisible(true)] + public sealed class ComparisonFileInfo + { + public string FileName { get; internal set; } + public DateTime LastWriteTime { get; internal set; } + public long Length { get; internal set; } + public int Length32 { get { return GetLength32(); } } + + internal ComparisonFileInfo() + { + } + + private int GetLength32() + { + return Tools.LengthTo32Bit(Length); + } + } + + [Guid("97F5222E-9379-4C24-9E50-E93C7334BBD5")] + [ClassInterface(Constants.ClassInterface)] + [ComVisible(true)] + public sealed class ComparisonDifference + { + public SynchronizationAction Action { get; internal set; } + public bool IsDirectory { get; internal set; } + public ComparisonFileInfo Local { get; internal set; } + public ComparisonFileInfo Remote { get; internal set; } + + internal ComparisonDifference() + { + } + } +} diff --git a/dotnet/RemoteFileInfo.cs b/dotnet/RemoteFileInfo.cs index dad9bd210..0c6f09b53 100644 --- a/dotnet/RemoteFileInfo.cs +++ b/dotnet/RemoteFileInfo.cs @@ -36,12 +36,7 @@ namespace WinSCP private int GetLength32() { - if ((Length < int.MinValue) || (Length > int.MaxValue)) - { - throw new OverflowException(string.Format(CultureInfo.CurrentCulture, "Size {0} cannot be represented using 32-bit value", Length)); - } - - return (int) Length; + return Tools.LengthTo32Bit(Length); } private void SetLength32(int value) diff --git a/dotnet/Session.cs b/dotnet/Session.cs index a87ac1c77..d3a1ab56f 100644 --- a/dotnet/Session.cs +++ b/dotnet/Session.cs @@ -847,77 +847,85 @@ namespace WinSCP { using (Logger.CreateCallstackAndLock()) { - if (options == null) - { - options = new TransferOptions(); - } - - CheckOpened(); - - if (removeFiles && (mode == SynchronizationMode.Both)) - { - throw Logger.WriteException(new ArgumentException("Cannot delete files in synchronization mode Both")); - } - - if (mirror && (mode == SynchronizationMode.Both)) - { - throw Logger.WriteException(new ArgumentException("Cannot mirror files in synchronization mode Both")); - } - - if ((criteria != SynchronizationCriteria.Time) && (mode == SynchronizationMode.Both)) - { - throw Logger.WriteException(new ArgumentException("Only Time criteria is allowed in synchronization mode Both")); - } - - string modeName; - switch (mode) - { - case SynchronizationMode.Local: - modeName = "local"; - break; - case SynchronizationMode.Remote: - modeName = "remote"; - break; - case SynchronizationMode.Both: - modeName = "both"; - break; - default: - throw Logger.WriteException(new ArgumentOutOfRangeException("mode")); - } - - string criteriaName; - switch (criteria) - { - case SynchronizationCriteria.None: - criteriaName = "none"; - break; - case SynchronizationCriteria.Time: - criteriaName = "time"; - break; - case SynchronizationCriteria.Size: - criteriaName = "size"; - break; - case SynchronizationCriteria.Either: - criteriaName = "either"; - break; - default: - throw Logger.WriteException(new ArgumentOutOfRangeException("criteria")); - } - - WriteCommand( - string.Format(CultureInfo.InvariantCulture, - "synchronize {0} {1} {2} {3} -criteria=\"{4}\" -- \"{5}\" \"{6}\"", - modeName, - BooleanSwitch(removeFiles, "delete"), - BooleanSwitch(mirror, "mirror"), - options.ToSwitches(), - criteriaName, - Tools.ArgumentEscape(localPath), Tools.ArgumentEscape(remotePath))); + DoSynchronizeDirectories(mode, localPath, remotePath, removeFiles, mirror, criteria, options, string.Empty); return ReadSynchronizeDirectories(); } } + private void DoSynchronizeDirectories( + SynchronizationMode mode, string localPath, string remotePath, bool removeFiles, bool mirror, + SynchronizationCriteria criteria, TransferOptions options, string additionalParameters) + { + if (options == null) + { + options = new TransferOptions(); + } + + CheckOpened(); + + if (removeFiles && (mode == SynchronizationMode.Both)) + { + throw Logger.WriteException(new ArgumentException("Cannot delete files in synchronization mode Both")); + } + + if (mirror && (mode == SynchronizationMode.Both)) + { + throw Logger.WriteException(new ArgumentException("Cannot mirror files in synchronization mode Both")); + } + + if ((criteria != SynchronizationCriteria.Time) && (mode == SynchronizationMode.Both)) + { + throw Logger.WriteException(new ArgumentException("Only Time criteria is allowed in synchronization mode Both")); + } + + string modeName; + switch (mode) + { + case SynchronizationMode.Local: + modeName = "local"; + break; + case SynchronizationMode.Remote: + modeName = "remote"; + break; + case SynchronizationMode.Both: + modeName = "both"; + break; + default: + throw Logger.WriteException(new ArgumentOutOfRangeException("mode")); + } + + string criteriaName; + switch (criteria) + { + case SynchronizationCriteria.None: + criteriaName = "none"; + break; + case SynchronizationCriteria.Time: + criteriaName = "time"; + break; + case SynchronizationCriteria.Size: + criteriaName = "size"; + break; + case SynchronizationCriteria.Either: + criteriaName = "either"; + break; + default: + throw Logger.WriteException(new ArgumentOutOfRangeException("criteria")); + } + + WriteCommand( + string.Format(CultureInfo.InvariantCulture, + "synchronize {0} {1} {2} {3} -criteria=\"{4}\" {5} -- \"{6}\" \"{7}\"", + modeName, + BooleanSwitch(removeFiles, "delete"), + BooleanSwitch(mirror, "mirror"), + options.ToSwitches(), + criteriaName, + additionalParameters, + Tools.ArgumentEscape(localPath), Tools.ArgumentEscape(remotePath))); + } + private SynchronizationResult ReadSynchronizeDirectories() { using (Logger.CreateCallstack()) @@ -990,6 +998,132 @@ namespace WinSCP } } + public ComparisonDifferenceCollection CompareDirectories( + SynchronizationMode mode, string localPath, string remotePath, + bool removeFiles, bool mirror = false, SynchronizationCriteria criteria = SynchronizationCriteria.Time, + TransferOptions options = null) + { + using (Logger.CreateCallstackAndLock()) + { + DoSynchronizeDirectories(mode, localPath, remotePath, removeFiles, mirror, criteria, options, "-preview"); + + return ReadCompareDirectories(); + } + } + + private ComparisonDifferenceCollection ReadCompareDirectories() + { + using (Logger.CreateCallstack()) + { + ComparisonDifferenceCollection result = new ComparisonDifferenceCollection(); + + using (ElementLogReader groupReader = _reader.WaitForGroupAndCreateLogReader()) + { + while (groupReader.TryWaitForNonEmptyElement("difference", LogReadFlags.ThrowFailures)) + { + using (ElementLogReader differenceReader = groupReader.CreateLogReader()) + { + ComparisonDifference difference = new ComparisonDifference(); + ComparisonFileInfo current = null; + + while (differenceReader.Read(0)) + { + if (differenceReader.GetEmptyElementValue("action", out string actionName)) + { + if ((difference.Local != null) || (difference.Remote != null)) + { + throw Logger.WriteException(new InvalidOperationException("Tag action after filename")); + } + if (actionName.Equals("uploadnew", StringComparison.OrdinalIgnoreCase)) + { + difference.Action = SynchronizationAction.UploadNew; + difference.Local = new ComparisonFileInfo(); + } + else if (actionName.Equals("downloadnew", StringComparison.OrdinalIgnoreCase)) + { + difference.Action = SynchronizationAction.DownloadNew; + difference.Remote = new ComparisonFileInfo(); + } + else if (actionName.Equals("uploadupdate", StringComparison.OrdinalIgnoreCase)) + { + difference.Action = SynchronizationAction.UploadUpdate; + difference.Local = new ComparisonFileInfo(); + difference.Remote = new ComparisonFileInfo(); + } + else if (actionName.Equals("downloadupdate", StringComparison.OrdinalIgnoreCase)) + { + difference.Action = SynchronizationAction.DownloadUpdate; + difference.Remote = new ComparisonFileInfo(); + difference.Local = new ComparisonFileInfo(); + } + else if (actionName.Equals("deleteremote", StringComparison.OrdinalIgnoreCase)) + { + difference.Action = SynchronizationAction.DeleteRemote; + difference.Remote = new ComparisonFileInfo(); + } + else if (actionName.Equals("deletelocal", StringComparison.OrdinalIgnoreCase)) + { + difference.Action = SynchronizationAction.DeleteLocal; + difference.Local = new ComparisonFileInfo(); + } + else + { + throw Logger.WriteException(new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Unknown synchronization action \"{0}\"", actionName))); + } + } + else if (differenceReader.GetEmptyElementValue("filename", out string value)) + { + if (current == null) + { + current = difference.Local ?? difference.Remote; + } + else if (current == difference.Local) + { + current = difference.Remote; + } + if (current == null) + { + throw Logger.WriteException(new InvalidOperationException("Unexpected filename tag")); + } + current.FileName = value; + } + else if (differenceReader.GetEmptyElementValue("modification", out value)) + { + if (current == null) + { + throw Logger.WriteException(new InvalidOperationException("Unexpected modification tag")); + } + current.LastWriteTime = XmlConvert.ToDateTime(value, XmlDateTimeSerializationMode.Local); + } + else if (differenceReader.GetEmptyElementValue("size", out value)) + { + if (current == null) + { + throw Logger.WriteException(new InvalidOperationException("Unexpected size tag")); + } + current.Length = long.Parse(value, CultureInfo.InvariantCulture); + } + } + + if (difference.Action == default(SynchronizationAction)) + { + throw Logger.WriteException(new InvalidOperationException("No action tag found")); + } + if (((difference.Local != null) && string.IsNullOrEmpty(difference.Local.FileName)) || + ((difference.Remote != null) && string.IsNullOrEmpty(difference.Remote.FileName))) + { + throw Logger.WriteException(new InvalidOperationException("Missing file information")); + } + + result.InternalAdd(difference); + } + } + } + + return result; + } + } + public CommandExecutionResult ExecuteCommand(string command) { using (Logger.CreateCallstackAndLock()) diff --git a/dotnet/internal/Tools.cs b/dotnet/internal/Tools.cs index 12f0f5eb4..4098d73fb 100644 --- a/dotnet/internal/Tools.cs +++ b/dotnet/internal/Tools.cs @@ -56,5 +56,15 @@ namespace WinSCP } } } + + public static int LengthTo32Bit(long length) + { + if (length < int.MinValue || length > int.MaxValue) + { + throw new OverflowException(string.Format(CultureInfo.CurrentCulture, "Size {0} cannot be represented using 32-bit value", length)); + } + + return (int)length; + } } } diff --git a/dotnet/interopcollections/ComparisonDifferenceCollection.cs b/dotnet/interopcollections/ComparisonDifferenceCollection.cs new file mode 100644 index 000000000..3ff712b70 --- /dev/null +++ b/dotnet/interopcollections/ComparisonDifferenceCollection.cs @@ -0,0 +1,88 @@ +using System.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace WinSCP +{ + [Guid("28957CC8-DEBC-48D0-841B-48AD3CB3B49F")] + [ClassInterface(Constants.ClassInterface)] + [ComVisible(true)] + public class ComparisonDifferenceCollection : ICollection + { + public ComparisonDifference this[int index] + { + get + { + return _helper[index]; + } + set + { + _helper[index] = value; + } + } + + #region ICollection Members + + public void Add(ComparisonDifference item) + { + _helper.Add(item); + } + + public void Clear() + { + _helper.Clear(); + } + + public bool Contains(ComparisonDifference item) + { + return _helper.Contains(item); + } + + public void CopyTo(ComparisonDifference[] array, int arrayIndex) + { + _helper.CopyTo(array, arrayIndex); + } + + public int Count + { + get { return _helper.Count; } + } + + public bool IsReadOnly + { + get { return _helper.IsReadOnly; } + } + + public bool Remove(ComparisonDifference item) + { + return _helper.Remove(item); + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return _helper.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + public IEnumerator GetEnumerator() + { + return _helper.GetEnumerator(); + } + + #endregion + + internal void InternalAdd(ComparisonDifference item) + { + _helper.InternalAdd(item); + } + + private readonly ReadOnlyInteropCollectionHelper _helper = new ReadOnlyInteropCollectionHelper(); + } +} diff --git a/source/core/Script.cpp b/source/core/Script.cpp index 48e3bc0bc..2f763a2d1 100644 --- a/source/core/Script.cpp +++ b/source/core/Script.cpp @@ -1842,6 +1842,8 @@ void __fastcall TScript::SynchronizePreview( const TSynchronizeChecklist::TItem * Item = Checklist->Item[Index]; if (Item->Checked) { + TDifferenceSessionAction Action(FTerminal->ActionLog, Item); + UnicodeString Message; UnicodeString LocalRecord = SynchronizeFileRecord(LocalDirectory, Item, true); UnicodeString RemoteRecord = SynchronizeFileRecord(RemoteDirectory, Item, false); diff --git a/source/core/SessionInfo.cpp b/source/core/SessionInfo.cpp index c634ecde8..1b85066f2 100644 --- a/source/core/SessionInfo.cpp +++ b/source/core/SessionInfo.cpp @@ -14,6 +14,7 @@ #include "TextsCore.h" #include "CoreMain.h" #include "Script.h" +#include //--------------------------------------------------------------------------- #pragma package(smart_init) //--------------------------------------------------------------------------- @@ -309,6 +310,59 @@ public: FFile = File->Duplicate(true); } + void __fastcall SynchronizeChecklistItem(const TSynchronizeChecklist::TItem * Item) + { + UnicodeString Action; + bool RecordLocal = false; + bool RecordRemote = false; + switch (Item->Action) + { + case TSynchronizeChecklist::saUploadNew: + Action = L"uploadnew"; + RecordLocal = true; + break; + case TSynchronizeChecklist::saDownloadNew: + Action = L"downloadnew"; + RecordRemote = true; + break; + case TSynchronizeChecklist::saUploadUpdate: + Action = L"uploadupdate"; + RecordLocal = true; + RecordRemote = true; + break; + case TSynchronizeChecklist::saDownloadUpdate: + Action = L"downloadupdate"; + RecordLocal = true; + RecordRemote = true; + break; + case TSynchronizeChecklist::saDeleteRemote: + Action = L"deleteremote"; + RecordRemote = true; + break; + case TSynchronizeChecklist::saDeleteLocal: + Action = L"deletelocal"; + RecordLocal = true; + break; + default: + DebugFail(); + break; + } + + Parameter(L"action", Action); + + if (RecordLocal) + { + UnicodeString FileName = TPath::Combine(Item->Local.Directory, Item->Local.FileName); + SynchronizeChecklistItemFileInfo(FileName, Item->IsDirectory, Item->Local); + } + if (RecordRemote) + { + UnicodeString FileName = UnixCombinePaths(Item->Remote.Directory, Item->Remote.FileName); + SynchronizeChecklistItemFileInfo(FileName, Item->IsDirectory, Item->Remote); + } + } + + protected: enum TState { Opened, Committed, RolledBack, Cancelled }; @@ -336,6 +390,7 @@ protected: case laStat: return L"stat"; case laChecksum: return L"checksum"; case laCwd: return L"cwd"; + case laDifference: return L"difference"; default: DebugFail(); return L""; } } @@ -374,6 +429,21 @@ protected: FLog->AddIndented(Indent + L""); } + void __fastcall SynchronizeChecklistItemFileInfo( + const UnicodeString & AFileName, bool IsDirectory, const TSynchronizeChecklist::TItem::TFileInfo FileInfo) + { + Parameter(L"type", XmlAttributeEscape(IsDirectory ? L'D' : L'-')); + FileName(XmlAttributeEscape(AFileName)); + if (!IsDirectory) + { + Parameter(L"size", XmlAttributeEscape(IntToStr(FileInfo.Size))); + } + if (FileInfo.ModificationFmt != mfNone) + { + Modification(FileInfo.Modification); + } + } + private: TActionLog * FLog; TLogAction FAction; @@ -658,6 +728,16 @@ __fastcall TCwdSessionAction::TCwdSessionAction(TActionLog * Log, const UnicodeS } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- +__fastcall TDifferenceSessionAction::TDifferenceSessionAction(TActionLog * Log, const TSynchronizeChecklist::TItem * Item) : + TSessionAction(Log, laDifference) +{ + if (FRecord != NULL) + { + FRecord->SynchronizeChecklistItem(Item); + } +} +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- TSessionInfo::TSessionInfo() { LoginTime = Now(); diff --git a/source/core/SessionInfo.h b/source/core/SessionInfo.h index e18ae632b..4ab15c18a 100644 --- a/source/core/SessionInfo.h +++ b/source/core/SessionInfo.h @@ -82,7 +82,7 @@ enum TLogLineType { llOutput, llInput, llStdError, llMessage, llException }; enum TLogAction { laUpload, laDownload, laTouch, laChmod, laMkdir, laRm, laMv, laCp, laCall, laLs, - laStat, laChecksum, laCwd + laStat, laChecksum, laCwd, laDifference }; //--------------------------------------------------------------------------- enum TCaptureOutputType { cotOutput, cotError, cotExitCode }; @@ -228,6 +228,12 @@ public: __fastcall TCwdSessionAction(TActionLog * Log, const UnicodeString & Path); }; //--------------------------------------------------------------------------- +class TDifferenceSessionAction : public TSessionAction +{ +public: + __fastcall TDifferenceSessionAction(TActionLog * Log, const TSynchronizeChecklist::TItem * Item); +}; +//--------------------------------------------------------------------------- class TSessionLog { public: