|
|
|
|
@@ -1,8 +1,9 @@
|
|
|
|
|
using NPoco;
|
|
|
|
|
using Umbraco.Cms.Infrastructure.Scoping;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
using NPoco;
|
|
|
|
|
using Umbraco.Cms.Core.Configuration.Models;
|
|
|
|
|
using Umbraco.Cms.Infrastructure.Scoping;
|
|
|
|
|
using Umbraco.Extensions;
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_0_0;
|
|
|
|
|
|
|
|
|
|
@@ -11,6 +12,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_0_0;
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class MigrateSystemDatesToUtc : UnscopedMigrationBase
|
|
|
|
|
{
|
|
|
|
|
private static readonly string[] UtcIdentifiers = ["Coordinated Universal Time", "UTC"];
|
|
|
|
|
|
|
|
|
|
private readonly IScopeProvider _scopeProvider;
|
|
|
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
|
private readonly IOptions<SystemDateMigrationSettings> _migrationSettings;
|
|
|
|
|
@@ -49,96 +52,153 @@ public class MigrateSystemDatesToUtc : UnscopedMigrationBase
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the local server timezone is not set, we detect it.
|
|
|
|
|
var timeZoneName = _migrationSettings.Value.LocalServerTimeZone;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(timeZoneName))
|
|
|
|
|
// Offsets and Windows name are lazy loaded as they are not always needed.
|
|
|
|
|
// This also allows using timezones that exist in the SQL Database but not on the local server.
|
|
|
|
|
(string TimeZoneName, Lazy<TimeSpan> TimeZoneOffset, Lazy<string> WindowsTimeZoneName) timeZone;
|
|
|
|
|
|
|
|
|
|
var configuredTimeZoneName = _migrationSettings.Value.LocalServerTimeZone;
|
|
|
|
|
if (configuredTimeZoneName.IsNullOrWhiteSpace() is false)
|
|
|
|
|
{
|
|
|
|
|
timeZoneName = _timeProvider.LocalTimeZone.Id;
|
|
|
|
|
_logger.LogInformation("Migrating system dates to UTC using the detected local server timezone: {TimeZoneName}.", timeZoneName);
|
|
|
|
|
timeZone = (
|
|
|
|
|
configuredTimeZoneName,
|
|
|
|
|
new Lazy<TimeSpan>(() => TimeZoneInfo.FindSystemTimeZoneById(configuredTimeZoneName).BaseUtcOffset),
|
|
|
|
|
new Lazy<string>(() => configuredTimeZoneName));
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation(
|
|
|
|
|
"Migrating system dates to UTC using the configured timezone: {TimeZoneName}.",
|
|
|
|
|
timeZone.TimeZoneName);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Migrating system dates to UTC using the configured local server timezone: {TimeZoneName}.", timeZoneName);
|
|
|
|
|
// If the local server timezone is not configured, we detect it.
|
|
|
|
|
TimeZoneInfo timeZoneInfo = _timeProvider.LocalTimeZone;
|
|
|
|
|
|
|
|
|
|
timeZone = (
|
|
|
|
|
timeZoneInfo.Id,
|
|
|
|
|
new Lazy<TimeSpan>(() => timeZoneInfo.BaseUtcOffset),
|
|
|
|
|
new Lazy<string>(() => GetWindowsTimeZoneId(timeZoneInfo)));
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation(
|
|
|
|
|
"Migrating system dates to UTC using the detected local server timezone: {TimeZoneName}.",
|
|
|
|
|
timeZone.TimeZoneName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the local server timezone is UTC, skip the migration.
|
|
|
|
|
if (string.Equals(timeZoneName, "Coordinated Universal Time", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
if (UtcIdentifiers.Contains(timeZone.TimeZoneName, StringComparer.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Skipping migration {MigrationName} as the local server timezone is UTC.", nameof(MigrateSystemDatesToUtc));
|
|
|
|
|
_logger.LogInformation(
|
|
|
|
|
"Skipping migration {MigrationName} as the local server timezone is UTC.",
|
|
|
|
|
nameof(MigrateSystemDatesToUtc));
|
|
|
|
|
Context.Complete();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TimeSpan timeZoneOffset = GetTimezoneOffset(timeZoneName);
|
|
|
|
|
|
|
|
|
|
using IScope scope = _scopeProvider.CreateScope();
|
|
|
|
|
using IDisposable notificationSuppression = scope.Notifications.Suppress();
|
|
|
|
|
|
|
|
|
|
MigrateDateColumn(scope, "cmsMember", "emailConfirmedDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "cmsMember", "lastLoginDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "cmsMember", "lastLockoutDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "cmsMember", "lastPasswordChangeDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoAccess", "createDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoAccess", "updateDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoAccessRule", "createDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoAccessRule", "updateDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoCreatedPackageSchema", "updateDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoContentVersion", "versionDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoContentVersionCleanupPolicy", "updated", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoContentVersionCultureVariation", "date", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoExternalLogin", "createDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoExternalLoginToken", "createDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoKeyValue", "updated", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoLog", "Datestamp", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoNode", "createDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoRelation", "datetime", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoServer", "registeredDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoServer", "lastNotifiedDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "createDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "updateDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "emailConfirmedDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "lastLockoutDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "lastPasswordChangeDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "lastLoginDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "invitedDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUserGroup", "createDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUserGroup", "updateDate", timeZoneName, timeZoneOffset);
|
|
|
|
|
MigrateDateColumn(scope, "cmsMember", "emailConfirmedDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "cmsMember", "lastLoginDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "cmsMember", "lastLockoutDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "cmsMember", "lastPasswordChangeDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoAccess", "createDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoAccess", "updateDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoAccessRule", "createDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoAccessRule", "updateDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoCreatedPackageSchema", "updateDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoContentVersion", "versionDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoContentVersionCleanupPolicy", "updated", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoContentVersionCultureVariation", "date", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoExternalLogin", "createDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoExternalLoginToken", "createDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoKeyValue", "updated", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoLog", "Datestamp", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoNode", "createDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoRelation", "datetime", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoServer", "registeredDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoServer", "lastNotifiedDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "createDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "updateDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "emailConfirmedDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "lastLockoutDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "lastPasswordChangeDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "lastLoginDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUser", "invitedDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUserGroup", "createDate", timeZone);
|
|
|
|
|
MigrateDateColumn(scope, "umbracoUserGroup", "updateDate", timeZone);
|
|
|
|
|
|
|
|
|
|
scope.Complete();
|
|
|
|
|
Context.Complete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static TimeSpan GetTimezoneOffset(string timeZoneName)
|
|
|
|
|
|
|
|
|
|
// We know the provided timezone name exists, as it's either detected or configured (and configuration has been validated).
|
|
|
|
|
=> TimeZoneInfo.FindSystemTimeZoneById(timeZoneName).BaseUtcOffset;
|
|
|
|
|
|
|
|
|
|
private void MigrateDateColumn(IScope scope, string tableName, string columName, string timezoneName, TimeSpan timeZoneOffset)
|
|
|
|
|
private static string GetWindowsTimeZoneId(TimeZoneInfo timeZone)
|
|
|
|
|
{
|
|
|
|
|
var offsetInMinutes = -timeZoneOffset.TotalMinutes;
|
|
|
|
|
var offSetInMinutesString = offsetInMinutes > 0
|
|
|
|
|
? $"+{offsetInMinutes}"
|
|
|
|
|
: $"{offsetInMinutes}";
|
|
|
|
|
if (timeZone.HasIanaId is false)
|
|
|
|
|
{
|
|
|
|
|
return timeZone.Id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Sql sql;
|
|
|
|
|
if (TimeZoneInfo.TryConvertIanaIdToWindowsId(timeZone.Id, out var windowsId) is false)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Could not update system dates to UTC as it was not possible to convert the detected local time zone IANA id '{timeZone.Id}' to a Windows Id necessary for updates with SQL Server. Please manually configure the 'Umbraco:CMS:SystemDateMigration:LocalServerTimeZone' app setting with a valid Windows time zone name.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return windowsId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void MigrateDateColumn(
|
|
|
|
|
IScope scope,
|
|
|
|
|
string tableName,
|
|
|
|
|
string columName,
|
|
|
|
|
(string Name, Lazy<TimeSpan> BaseOffset, Lazy<string> WindowsName) timeZone)
|
|
|
|
|
{
|
|
|
|
|
if (DatabaseType == DatabaseType.SQLite)
|
|
|
|
|
{
|
|
|
|
|
// SQLite does not support AT TIME ZONE, but we can use the offset to update the dates. It won't take account of daylight saving time, but
|
|
|
|
|
// given these are historical dates in expected non-production environments, that are unlikely to be necessary to be 100% accurate, this is acceptable.
|
|
|
|
|
sql = Sql($"UPDATE {tableName} SET {columName} = DATETIME({columName}, '{offSetInMinutesString} minutes')");
|
|
|
|
|
MigrateDateColumnSQLite(scope, tableName, columName, timeZone.Name, timeZone.BaseOffset.Value);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
sql = Sql($"UPDATE {tableName} SET {columName} = {columName} AT TIME ZONE '{timezoneName}' AT TIME ZONE 'UTC'");
|
|
|
|
|
MigrateDateColumnSqlServer(scope, tableName, columName, timeZone.WindowsName.Value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void MigrateDateColumnSQLite(
|
|
|
|
|
IScope scope,
|
|
|
|
|
string tableName,
|
|
|
|
|
string columName,
|
|
|
|
|
string timezoneName,
|
|
|
|
|
TimeSpan timezoneOffset)
|
|
|
|
|
{
|
|
|
|
|
// SQLite does not support AT TIME ZONE, but we can use the offset to update the dates. It won't take account of daylight saving time, but
|
|
|
|
|
// given these are historical dates in expected non-production environments, that are unlikely to be necessary to be 100% accurate, this is acceptable.
|
|
|
|
|
|
|
|
|
|
var offsetInMinutes = -timezoneOffset.TotalMinutes;
|
|
|
|
|
var offsetInMinutesString = offsetInMinutes > 0
|
|
|
|
|
? $"+{offsetInMinutes}"
|
|
|
|
|
: $"{offsetInMinutes}";
|
|
|
|
|
|
|
|
|
|
Sql sql = Sql($"UPDATE {tableName} SET {columName} = DATETIME({columName}, '{offsetInMinutesString} minutes')");
|
|
|
|
|
|
|
|
|
|
scope.Database.Execute(sql);
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation(
|
|
|
|
|
"Migrated {TableName}.{ColumnName} from local server timezone of {TimeZoneName} ({OffSetInMinutes} minutes) to UTC.",
|
|
|
|
|
"Migrated {TableName}.{ColumnName} from timezone {TimeZoneName} ({OffsetInMinutes}) to UTC.",
|
|
|
|
|
tableName,
|
|
|
|
|
columName,
|
|
|
|
|
timezoneName,
|
|
|
|
|
offSetInMinutesString);
|
|
|
|
|
offsetInMinutesString);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void MigrateDateColumnSqlServer(IScope scope, string tableName, string columName, string timeZoneName)
|
|
|
|
|
{
|
|
|
|
|
Sql sql = Sql($"UPDATE {tableName} SET {columName} = {columName} AT TIME ZONE '{timeZoneName}' AT TIME ZONE 'UTC'");
|
|
|
|
|
|
|
|
|
|
scope.Database.Execute(sql);
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation(
|
|
|
|
|
"Migrated {TableName}.{ColumnName} from timezone {TimeZoneName} to UTC.",
|
|
|
|
|
tableName,
|
|
|
|
|
columName,
|
|
|
|
|
timeZoneName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|