Skip to content

Commit

Permalink
major: rework timezone conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
robinrodricks committed Oct 12, 2020
1 parent 0980297 commit a968e46
Show file tree
Hide file tree
Showing 14 changed files with 115 additions and 135 deletions.
2 changes: 1 addition & 1 deletion FluentFTP.CSharpExamples/CustomParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private static FtpListItem ParseUnixList(string buf, List<FtpCapability> capabil
// to convert it to a DateTime object and use it for directories.
////
if ((!capabilities.Contains(FtpCapability.MDTM) || item.Type == FtpFileSystemObjectType.Directory) && m.Groups["modify"].Value.Length > 0) {
item.Modified = m.Groups["modify"].Value.GetFtpDate(client.TimeConversion);
item.Modified = client.ParseFtpDate(m.Groups["modify"].Value);
}

if (m.Groups["size"].Value.Length > 0) {
Expand Down
1 change: 1 addition & 0 deletions FluentFTP/Client/FtpClient_Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ protected FtpClient CloneConnection() {
conn.ListingParser = ListingParser;
conn.ListingCulture = ListingCulture;
conn.TimeOffset = TimeOffset;
conn.TimeConversion = TimeConversion;
conn.RetryAttempts = RetryAttempts;
conn.UploadRateLimit = UploadRateLimit;
conn.DownloadZeroByteFiles = DownloadZeroByteFiles;
Expand Down
4 changes: 2 additions & 2 deletions FluentFTP/Client/FtpClient_FileCompare.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public FtpCompareResult CompareFile(string localPath, string remotePath, FtpComp

// check file size
var localDate = FtpFileStream.GetFileDateModifiedUtc(localPath);
var remoteDate = GetModifiedTime(remotePath, FtpDate.UTC);
var remoteDate = GetModifiedTime(remotePath);
if (!localDate.Equals(remoteDate)) {
return FtpCompareResult.NotEqual;
}
Expand Down Expand Up @@ -148,7 +148,7 @@ public async Task<FtpCompareResult> CompareFileAsync(string localPath, string re

// check file size
var localDate = await FtpFileStream.GetFileDateModifiedUtcAsync(localPath, token);
var remoteDate = await GetModifiedTimeAsync(remotePath, FtpDate.UTC, token);
var remoteDate = await GetModifiedTimeAsync(remotePath, token);
if (!localDate.Equals(remoteDate)) {
return FtpCompareResult.NotEqual;
}
Expand Down
95 changes: 21 additions & 74 deletions FluentFTP/Client/FtpClient_FileProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,19 +382,18 @@ private async Task GetFileSizeInternalAsync(string path, CancellationToken token
#region Get Modified Time

/// <summary>
/// Gets the modified time of a remote file
/// Gets the modified time of a remote file.
/// </summary>
/// <param name="path">The full path to the file</param>
/// <param name="type">Return the date in local timezone or UTC? Use FtpDate.Original to disable timezone conversion.</param>
/// <returns>The modified time, or <see cref="DateTime.MinValue"/> if there was a problem</returns>
/// <example><code source="..\Examples\GetModifiedTime.cs" lang="cs" /></example>
public virtual DateTime GetModifiedTime(string path, FtpDate type = FtpDate.Original) {
public virtual DateTime GetModifiedTime(string path) {
// verify args
if (path.IsBlank()) {
throw new ArgumentException("Required parameter is null or blank.", "path");
}

LogFunc(nameof(GetModifiedTime), new object[] { path, type });
LogFunc(nameof(GetModifiedTime), new object[] { path });

var date = DateTime.MinValue;
FtpReply reply;
Expand All @@ -405,20 +404,7 @@ public virtual DateTime GetModifiedTime(string path, FtpDate type = FtpDate.Orig

// get modified date of a file
if ((reply = Execute("MDTM " + path.GetFtpPath())).Success) {
date = reply.Message.GetFtpDate(TimeConversion);

// convert server timezone to UTC, based on the TimeOffset property
if (type != FtpDate.Original && m_listParser.HasTimeOffset) {
date = date - m_listParser.TimeOffset;
}

// convert to local time if wanted
#if !CORE
if (type == FtpDate.Local) {
date = TimeZone.CurrentTimeZone.ToLocalTime(date);
}

#endif
date = ParseFtpDate(reply.Message);
}

#if !CORE14
Expand All @@ -429,23 +415,22 @@ public virtual DateTime GetModifiedTime(string path, FtpDate type = FtpDate.Orig
}

#if !ASYNC
private delegate DateTime AsyncGetModifiedTime(string path, FtpDate type);
private delegate DateTime AsyncGetModifiedTime(string path);

/// <summary>
/// Begins an asynchronous operation to get the modified time of a remote file
/// </summary>
/// <param name="path">The full path to the file</param>
/// <param name="type">Return the date in local timezone or UTC? Use FtpDate.Original to disable timezone conversion.</param>
/// <param name="callback">Async callback</param>
/// <param name="state">State object</param>
/// <returns>IAsyncResult</returns>
/// <example><code source="..\Examples\BeginGetModifiedTime.cs" lang="cs" /></example>
public IAsyncResult BeginGetModifiedTime(string path, FtpDate type, AsyncCallback callback, object state) {
public IAsyncResult BeginGetModifiedTime(string path, AsyncCallback callback, object state) {
IAsyncResult ar;
AsyncGetModifiedTime func;

lock (m_asyncmethods) {
ar = (func = GetModifiedTime).BeginInvoke(path, type, callback, state);
ar = (func = GetModifiedTime).BeginInvoke(path, callback, state);
m_asyncmethods.Add(ar, func);
}

Expand All @@ -468,36 +453,22 @@ public DateTime EndGetModifiedTime(IAsyncResult ar) {
/// Gets the modified time of a remote file asynchronously
/// </summary>
/// <param name="path">The full path to the file</param>
/// <param name="type">Return the date in local timezone or UTC? Use FtpDate.Original to disable timezone conversion.</param>
/// <param name="token">The token that can be used to cancel the entire process</param>
/// <returns>The modified time, or <see cref="DateTime.MinValue"/> if there was a problem</returns>
public async Task<DateTime> GetModifiedTimeAsync(string path, FtpDate type = FtpDate.Original, CancellationToken token = default(CancellationToken)) {
public async Task<DateTime> GetModifiedTimeAsync(string path, CancellationToken token = default(CancellationToken)) {
// verify args
if (path.IsBlank()) {
throw new ArgumentException("Required parameter is null or blank.", "path");
}

LogFunc(nameof(GetModifiedTimeAsync), new object[] { path, type });
LogFunc(nameof(GetModifiedTimeAsync), new object[] { path });

var date = DateTime.MinValue;
FtpReply reply;

// get modified date of a file
if ((reply = await ExecuteAsync("MDTM " + path.GetFtpPath(), token)).Success) {
date = reply.Message.GetFtpDate(TimeConversion);

// convert server timezone to UTC, based on the TimeOffset property
if (type != FtpDate.Original && m_listParser.HasTimeOffset) {
date = date - m_listParser.TimeOffset;
}

// convert to local time if wanted
#if !CORE
if (type == FtpDate.Local) {
date = TimeZone.CurrentTimeZone.ToLocalTime(date);
}

#endif
date = ParseFtpDate(reply.Message);
}

return date;
Expand All @@ -513,8 +484,7 @@ public DateTime EndGetModifiedTime(IAsyncResult ar) {
/// </summary>
/// <param name="path">The full path to the file</param>
/// <param name="date">The new modified date/time value</param>
/// <param name="type">Is the date provided in local timezone or UTC? Use FtpDate.Original to disable timezone conversion.</param>
public virtual void SetModifiedTime(string path, DateTime date, FtpDate type = FtpDate.Original) {
public virtual void SetModifiedTime(string path, DateTime date) {
// verify args
if (path.IsBlank()) {
throw new ArgumentException("Required parameter is null or blank.", "path");
Expand All @@ -524,28 +494,18 @@ public virtual void SetModifiedTime(string path, DateTime date, FtpDate type = F
throw new ArgumentException("Required parameter is null or blank.", "date");
}

LogFunc(nameof(SetModifiedTime), new object[] { path, date, type });
LogFunc(nameof(SetModifiedTime), new object[] { path, date });

FtpReply reply;

#if !CORE14
lock (m_lock) {
#endif

// convert local to UTC if wanted
#if !CORE
if (type == FtpDate.Local) {
date = TimeZone.CurrentTimeZone.ToUniversalTime(date);
}
#endif

// convert UTC to server timezone, based on the TimeOffset property
if (type != FtpDate.Original && m_listParser.HasTimeOffset) {
date = date + m_listParser.TimeOffset;
}
// calculate the final date string with the timezone conversion
var timeStr = GenerateFtpDate(date);

// set modified date of a file
var timeStr = date.ToString("yyyyMMddHHmmss");
if ((reply = Execute("MFMT " + timeStr + " " + path.GetFtpPath())).Success) {
}

Expand All @@ -556,23 +516,21 @@ public virtual void SetModifiedTime(string path, DateTime date, FtpDate type = F
}

#if !ASYNC
private delegate void AsyncSetModifiedTime(string path, DateTime date, FtpDate type);
private delegate void AsyncSetModifiedTime(string path, DateTime date);

/// <summary>
/// Begins an asynchronous operation to get the modified time of a remote file
/// </summary>
/// <param name="path">The full path to the file</param>
/// <param name="date">The new modified date/time value</param>
/// <param name="type">Is the date provided in local timezone or UTC? Use FtpDate.Original to disable timezone conversion.</param>
/// <param name="callback">Async callback</param>
/// <param name="state">State object</param>
/// <returns>IAsyncResult</returns>
public IAsyncResult BeginSetModifiedTime(string path, DateTime date, FtpDate type, AsyncCallback callback, object state) {
public IAsyncResult BeginSetModifiedTime(string path, DateTime date, AsyncCallback callback, object state) {
IAsyncResult ar;
AsyncSetModifiedTime func;

lock (m_asyncmethods) {
ar = (func = SetModifiedTime).BeginInvoke(path, date, type, callback, state);
ar = (func = SetModifiedTime).BeginInvoke(path, date, callback, state);
m_asyncmethods.Add(ar, func);
}

Expand All @@ -595,9 +553,8 @@ public void EndSetModifiedTime(IAsyncResult ar) {
/// </summary>
/// <param name="path">The full path to the file</param>
/// <param name="date">The new modified date/time value</param>
/// <param name="type">Is the date provided in local timezone or UTC? Use FtpDate.Original to disable timezone conversion.</param>
/// <param name="token">The token that can be used to cancel the entire process</param>
public async Task SetModifiedTimeAsync(string path, DateTime date, FtpDate type = FtpDate.Original, CancellationToken token = default(CancellationToken)) {
public async Task SetModifiedTimeAsync(string path, DateTime date, CancellationToken token = default(CancellationToken)) {
// verify args
if (path.IsBlank()) {
throw new ArgumentException("Required parameter is null or blank.", "path");
Expand All @@ -607,24 +564,14 @@ public void EndSetModifiedTime(IAsyncResult ar) {
throw new ArgumentException("Required parameter is null or blank.", "date");
}

LogFunc(nameof(SetModifiedTimeAsync), new object[] { path, date, type });
LogFunc(nameof(SetModifiedTimeAsync), new object[] { path, date });

FtpReply reply;

// convert local to UTC if wanted
#if !CORE
if (type == FtpDate.Local) {
date = TimeZone.CurrentTimeZone.ToUniversalTime(date);
}
#endif

// convert UTC to server timezone, based on the TimeOffset property
if (type != FtpDate.Original && m_listParser.HasTimeOffset) {
date = date + m_listParser.TimeOffset;
}
// calculate the final date string with the timezone conversion
var timeStr = GenerateFtpDate(date);

// set modified date of a file
var timeStr = date.ToString("yyyyMMddHHmmss");
if ((reply = await ExecuteAsync("MFMT " + timeStr + " " + path.GetFtpPath(), token)).Success) {
}
}
Expand Down
2 changes: 1 addition & 1 deletion FluentFTP/Client/FtpClient_Listing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ public FtpListItem EndGetObjectInfo(IAsyncResult ar) {

// Get the accurate date modified using another MDTM command
if (result != null && dateModified && HasFeature(FtpCapability.MDTM)) {
result.Modified = await GetModifiedTimeAsync(path, FtpDate.Original, token);
result.Modified = await GetModifiedTimeAsync(path, token);
}

return result;
Expand Down
16 changes: 9 additions & 7 deletions FluentFTP/Client/FtpClient_Properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -680,8 +680,6 @@ public CultureInfo ListingCulture {
set => m_parserCulture = value;
}

private double m_timeDiff = 0;


/// <summary>
/// Detect if your FTP server supports the recursive LIST command (LIST -R).
Expand Down Expand Up @@ -710,6 +708,9 @@ public bool RecursiveList {
}


public TimeSpan m_timeOffset = new TimeSpan();
private double m_timeDiff = 0;

/// <summary>
/// Time difference between server and client, in hours.
/// If the server is located in New York and you are in London then the time difference is -5 hours.
Expand All @@ -722,18 +723,19 @@ public double TimeOffset {
// configure parser
var hours = (int)Math.Floor(m_timeDiff);
var mins = (int)Math.Floor((m_timeDiff - Math.Floor(m_timeDiff)) * 60);
m_listParser.TimeOffset = new TimeSpan(hours, mins, 0);
m_listParser.HasTimeOffset = m_timeDiff != 0;
m_timeOffset = new TimeSpan(hours, mins, 0);
}
}

private DateTimeStyles m_timeConversion = DateTimeStyles.AssumeUniversal;
private FtpDate m_timeConversion = FtpDate.Original;

/// <summary>
/// Controls how timestamps returned by the server are converted.
/// The default setting assumes that all servers return UTC.
/// FtpDate.Original will preserve the value that the server sends.
/// FtpDate.Local assumes that the server timestamp is in UTC and attempts to convert it to the local timezone.
/// FtpDate.UTC assumes that the server timestamp is in Local Time and attempts to convert it to UTC.
/// </summary>
public DateTimeStyles TimeConversion {
public FtpDate TimeConversion {
get => m_timeConversion;
set {
m_timeConversion = value;
Expand Down
63 changes: 63 additions & 0 deletions FluentFTP/Client/FtpClient_Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text;
using System.Reflection;
using System.Collections.Generic;
using System.Globalization;
using System.Diagnostics;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -354,5 +355,67 @@ private static string TraceLevelPrefix(FtpTraceLevel level) {
}

#endregion

#region Date/Time


private static string[] FtpDateFormats = { "yyyyMMddHHmmss", "yyyyMMddHHmmss'.'f", "yyyyMMddHHmmss'.'ff", "yyyyMMddHHmmss'.'fff", "MMM dd yyyy", "MMM d yyyy", "MMM dd HH:mm", "MMM d HH:mm" };

/// <summary>
/// Tries to convert the string FTP date representation into a <see cref="DateTime"/> object
/// </summary>
/// <param name="date">The date string</param>
/// <returns>A <see cref="DateTime"/> object representing the date, or <see cref="DateTime.MinValue"/> if there was a problem</returns>
public DateTime ParseFtpDate(string date) {
DateTime parsed;

// parse the raw timestamp without performing any timezone conversions
if (DateTime.TryParseExact(date, FtpDateFormats, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out parsed)) {

// convert server timezone to UTC, based on the TimeOffset property
if (m_timeConversion == FtpDate.TimeOffset) {
parsed = parsed - m_timeOffset;
}

// convert to local time if wanted
#if !CORE
if (m_timeConversion == FtpDate.UTCToLocal) {
parsed = TimeZone.CurrentTimeZone.ToLocalTime(parsed);
}

#endif
// return the final parsed date value
return parsed;
}


return DateTime.MinValue;
}

/// <summary>
/// Generates an FTP date-string when provided a date value
/// </summary>
/// <param name="date">The date value</param>
/// <returns>A <see cref="DateTime"/> object representing the date, or <see cref="DateTime.MinValue"/> if there was a problem</returns>
public string GenerateFtpDate(DateTime date) {

// convert local to UTC if wanted
#if !CORE
if (m_timeConversion == FtpDate.UTCToLocal) {
date = TimeZone.CurrentTimeZone.ToUniversalTime(date);
}
#endif

// convert UTC to server timezone, based on the TimeOffset property
if (m_timeConversion == FtpDate.TimeOffset) {
date = date + m_timeOffset;
}

// generate final pretty printed date
var timeStr = date.ToString("yyyyMMddHHmmss");
return timeStr;
}

#endregion
}
}
Loading

0 comments on commit a968e46

Please sign in to comment.