Merge branch 'master' into feature/bump-cocol-183
This commit is contained in:
@@ -385,11 +385,8 @@ public static partial class ExtensionMethods
|
||||
/// <param name="self">The <see cref="SqliteCommand"/> instance to execute on.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
|
||||
/// <returns>A task that when awaited contains the first column of the first row in the result set as an Int64, or -1 if no rows are returned or the value cannot be converted.</returns>
|
||||
public static async Task<long> ExecuteScalarInt64Async(this SqliteCommand self, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteScalarInt64Async(self, true, null, null, -1, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
public static Task<long> ExecuteScalarInt64Async(this SqliteCommand self, CancellationToken cancellationToken)
|
||||
=> ExecuteScalarInt64Async(self, true, null, null, -1, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command asynchronously and returns the first column of the first row in the result set as an Int64.
|
||||
@@ -398,11 +395,8 @@ public static partial class ExtensionMethods
|
||||
/// <param name="defaultvalue">The default value to return if no rows are returned or the value cannot be converted.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
|
||||
/// <returns>A task that when awaited contains the first column of the first row in the result set as an Int64, or -1 if no rows are returned or the value cannot be converted.</returns>
|
||||
public static async Task<long> ExecuteScalarInt64Async(this SqliteCommand self, long defaultvalue, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteScalarInt64Async(self, true, null, null, defaultvalue, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
public static Task<long> ExecuteScalarInt64Async(this SqliteCommand self, long defaultvalue, CancellationToken cancellationToken)
|
||||
=> ExecuteScalarInt64Async(self, true, null, null, defaultvalue, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command asynchronously and returns the first column of the first row in the result set as an Int64.
|
||||
@@ -411,11 +405,8 @@ public static partial class ExtensionMethods
|
||||
/// <param name="writeLog">Whether to write a log entry.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
|
||||
/// <returns>A task that when awaited contains the first column of the first row in the result set as an Int64, or -1 if no rows are returned or the value cannot be converted.</returns>
|
||||
public static async Task<long> ExecuteScalarInt64Async(this SqliteCommand self, bool writeLog, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteScalarInt64Async(self, writeLog, null, null, -1, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
public static Task<long> ExecuteScalarInt64Async(this SqliteCommand self, bool writeLog, CancellationToken cancellationToken)
|
||||
=> ExecuteScalarInt64Async(self, writeLog, null, null, -1, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command asynchronously and returns the first column of the first row in the result set as an Int64.
|
||||
@@ -425,11 +416,8 @@ public static partial class ExtensionMethods
|
||||
/// <param name="defaultvalue">The default value to return if no rows are returned or the value cannot be converted.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
|
||||
/// <returns>A task that when awaited contains the first column of the first row in the result set as an Int64, or the default value if no rows are returned or the value cannot be converted.</returns>
|
||||
public static async Task<long> ExecuteScalarInt64Async(this SqliteCommand self, bool writeLog, long defaultvalue, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteScalarInt64Async(self, writeLog, null, null, defaultvalue, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
public static Task<long> ExecuteScalarInt64Async(this SqliteCommand self, bool writeLog, long defaultvalue, CancellationToken cancellationToken)
|
||||
=> ExecuteScalarInt64Async(self, writeLog, null, null, defaultvalue, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command asynchronously and returns the first column of the first row in the result set as an Int64.
|
||||
@@ -438,11 +426,8 @@ public static partial class ExtensionMethods
|
||||
/// <param name="cmd">The command text to execute.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
|
||||
/// <returns>A task that when awaited contains the first column of the first row in the result set as an Int64, or the default value if no rows are returned or the value cannot be converted.</returns>
|
||||
public static async Task<long> ExecuteScalarInt64Async(this SqliteCommand self, string? cmd, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteScalarInt64Async(self, true, cmd, null, -1, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
public static Task<long> ExecuteScalarInt64Async(this SqliteCommand self, string? cmd, CancellationToken cancellationToken)
|
||||
=> ExecuteScalarInt64Async(self, true, cmd, null, -1, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command asynchronously and returns the first column of the first row in the result set as an Int64.
|
||||
@@ -452,11 +437,8 @@ public static partial class ExtensionMethods
|
||||
/// <param name="defaultvalue">The default value to return if no rows are returned or the value cannot be converted.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
|
||||
/// <returns>A task that when awaited contains the first column of the first row in the result set as an Int64, or the default value if no rows are returned or the value cannot be converted.</returns>
|
||||
public static async Task<long> ExecuteScalarInt64Async(this SqliteCommand self, string? cmd, long defaultvalue, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteScalarInt64Async(self, true, cmd, null, defaultvalue, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
public static Task<long> ExecuteScalarInt64Async(this SqliteCommand self, string? cmd, long defaultvalue, CancellationToken cancellationToken)
|
||||
=> ExecuteScalarInt64Async(self, true, cmd, null, defaultvalue, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command asynchronously and returns the first column of the first row in the result set as an Int64.
|
||||
@@ -466,11 +448,8 @@ public static partial class ExtensionMethods
|
||||
/// <param name="values">The values to use as parameters.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
|
||||
/// <returns>A task that when awaited contains the first column of the first row in the result set as an Int64, or the default value if no rows are returned or the value cannot be converted.</returns>
|
||||
public static async Task<long> ExecuteScalarInt64Async(this SqliteCommand self, string? cmd, Dictionary<string, object?>? values, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteScalarInt64Async(self, true, cmd, values, -1, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
public static Task<long> ExecuteScalarInt64Async(this SqliteCommand self, string? cmd, Dictionary<string, object?>? values, CancellationToken cancellationToken)
|
||||
=> ExecuteScalarInt64Async(self, true, cmd, values, -1, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command asynchronously and returns the first column of the first row in the result set as an Int64.
|
||||
@@ -481,11 +460,8 @@ public static partial class ExtensionMethods
|
||||
/// <param name="defaultvalue">The default value to return if no rows are returned or the value cannot be converted.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
|
||||
/// <returns>A task that when awaited contains the first column of the first row in the result set as an Int64, or the default value if no rows are returned or the value cannot be converted.</returns>
|
||||
public static async Task<long> ExecuteScalarInt64Async(this SqliteCommand self, string? cmd, Dictionary<string, object?>? values, long defaultvalue, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteScalarInt64Async(self, true, cmd, values, defaultvalue, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
public static Task<long> ExecuteScalarInt64Async(this SqliteCommand self, string? cmd, Dictionary<string, object?>? values, long defaultvalue, CancellationToken cancellationToken)
|
||||
=> ExecuteScalarInt64Async(self, true, cmd, values, defaultvalue, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command asynchronously and returns the first column of the first row in the result set as an Int64.
|
||||
@@ -496,11 +472,8 @@ public static partial class ExtensionMethods
|
||||
/// <param name="values">The values to set as parameters.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
|
||||
/// <returns>A task that when awaited contains the first column of the first row in the result set as an Int64, or the default value if no rows are returned or the value cannot be converted.</returns>
|
||||
public static async Task<long> ExecuteScalarInt64Async(this SqliteCommand self, bool writeLog, string? cmd, Dictionary<string, object?>? values, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteScalarInt64Async(self, writeLog, cmd, values, -1, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
public static Task<long> ExecuteScalarInt64Async(this SqliteCommand self, bool writeLog, string? cmd, Dictionary<string, object?>? values, CancellationToken cancellationToken)
|
||||
=> ExecuteScalarInt64Async(self, writeLog, cmd, values, -1, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command asynchronously and returns the first column of the first row in the result set as an Int64.
|
||||
@@ -801,13 +774,13 @@ public static partial class ExtensionMethods
|
||||
if (p.Value == null || p.Value == DBNull.Value)
|
||||
v = "NULL";
|
||||
else if (p.Value is string)
|
||||
v = $"\"{p.Value}\"";
|
||||
v = Library.Utility.Utility.FormatInvariant($"\"{p.Value}\"");
|
||||
else if (p.Value is DateTime dt)
|
||||
v = $"\"{dt:O}\"";
|
||||
v = Library.Utility.Utility.FormatInvariant($"\"{dt:O}\"");
|
||||
else if (p.Value is byte[] bytes)
|
||||
v = $"X'{BitConverter.ToString(bytes).Replace("-", "")}'";
|
||||
v = Library.Utility.Utility.FormatInvariant($"X'{BitConverter.ToString(bytes).Replace("-", "")}'");
|
||||
else
|
||||
v = p.Value.ToString() ?? "NULL";
|
||||
v = p.Value?.FormatInvariant() ?? "NULL";
|
||||
|
||||
// Replace all occurrences of the parameter (with word boundary)
|
||||
txt = Regex.Replace(txt, $@"\B{Regex.Escape(paramName)}\b", v, RegexOptions.IgnoreCase);
|
||||
@@ -835,9 +808,7 @@ public static partial class ExtensionMethods
|
||||
/// <param name="values">The values to use as parameters. The parameters must already be added.</param>
|
||||
/// <returns>The number of rows affected</returns>
|
||||
public static int ExecuteNonQuery(this IDbCommand self, string cmd)
|
||||
{
|
||||
return ExecuteNonQuery(self, true, cmd);
|
||||
}
|
||||
=> ExecuteNonQuery(self, true, cmd);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command and returns the number of rows affected.
|
||||
@@ -847,9 +818,7 @@ public static partial class ExtensionMethods
|
||||
/// <param name="values">The values to use as parameters. The parameters must already be added.</param>
|
||||
/// <returns>The number of rows affected</returns>
|
||||
public static int ExecuteNonQuery(this IDbCommand self, string cmd, Dictionary<string, object?> values)
|
||||
{
|
||||
return ExecuteNonQuery(self, true, cmd, values);
|
||||
}
|
||||
=> ExecuteNonQuery(self, true, cmd, values);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command and returns the number of rows affected.
|
||||
@@ -906,9 +875,7 @@ public static partial class ExtensionMethods
|
||||
/// <param name="cmd">The command string to execute</param>
|
||||
/// <returns>The scalar value of the first row</returns>
|
||||
public static object? ExecuteScalar(this IDbCommand self, string cmd)
|
||||
{
|
||||
return ExecuteScalar(self, true, cmd);
|
||||
}
|
||||
=> ExecuteScalar(self, true, cmd);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command and returns the scalar value of the first row.
|
||||
@@ -934,9 +901,7 @@ public static partial class ExtensionMethods
|
||||
/// <param name="defaultvalue">The default value to return if no value is found</param>
|
||||
/// <returns>The scalar int64 value of the first row as a string</returns>
|
||||
public static long ExecuteScalarInt64(this IDbCommand self, bool writeLog, long defaultvalue = -1)
|
||||
{
|
||||
return ExecuteScalarInt64(self, writeLog, null, defaultvalue);
|
||||
}
|
||||
=> ExecuteScalarInt64(self, writeLog, null, defaultvalue);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command and returns the scalar int64 value of the first row as a string.
|
||||
@@ -945,9 +910,7 @@ public static partial class ExtensionMethods
|
||||
/// <param name="defaultvalue">The default value to return if no value is found</param>
|
||||
/// <returns>The scalar int64 value of the first row as a string</returns>
|
||||
public static long ExecuteScalarInt64(this IDbCommand self, long defaultvalue = -1)
|
||||
{
|
||||
return ExecuteScalarInt64(self, true, null, defaultvalue);
|
||||
}
|
||||
=> ExecuteScalarInt64(self, true, null, defaultvalue);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command and returns the scalar int64 value of the first row as a string.
|
||||
@@ -970,9 +933,7 @@ public static partial class ExtensionMethods
|
||||
/// <param name="defaultvalue">The default value to return if no value is found</param>
|
||||
/// <returns>The scalar int64 value of the first row as a string</returns>
|
||||
public static long ExecuteScalarInt64(this IDbCommand self, string? cmd, long defaultvalue = -1)
|
||||
{
|
||||
return ExecuteScalarInt64(self, true, cmd, defaultvalue);
|
||||
}
|
||||
=> ExecuteScalarInt64(self, true, cmd, defaultvalue);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command and returns the scalar int64 value of the first row as a string.
|
||||
@@ -1003,9 +964,7 @@ public static partial class ExtensionMethods
|
||||
/// <param name="values">The values to use as parameters. The parameters must already be added.</param>
|
||||
/// <returns>A <see cref="IDataReader"/> instance</returns>
|
||||
public static IDataReader ExecuteReader(this IDbCommand self, string cmd, Dictionary<string, object?>? values)
|
||||
{
|
||||
return ExecuteReader(self, true, cmd, values);
|
||||
}
|
||||
=> ExecuteReader(self, true, cmd, values);
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -1015,9 +974,7 @@ public static partial class ExtensionMethods
|
||||
/// <param name="cmd">The command string to execute</param>
|
||||
/// <returns>A <see cref="IDataReader"/> instance</returns>
|
||||
public static IDataReader ExecuteReader(this IDbCommand self, string cmd)
|
||||
{
|
||||
return ExecuteReader(self, true, cmd);
|
||||
}
|
||||
=> ExecuteReader(self, true, cmd);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command and returns a data reader.
|
||||
@@ -1175,26 +1132,6 @@ public static partial class ExtensionMethods
|
||||
return cmd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the transaction for the given connection and executes a "BEGIN IMMEDIATE" command to ensure that the transaction can be comitted or rolled back.
|
||||
/// This works around a quirk in SQLite where the transaction is not initialized until the first command is executed.
|
||||
/// This means that if the transaction is rolled back or committed with no commands executed, it will fail with an exception.
|
||||
/// </summary>
|
||||
/// <param name="self">The connection to create the transaction on</param>
|
||||
/// <returns>The transaction</returns>
|
||||
//public static IDbTransaction BeginTransactionSafe(this IDbConnection self)
|
||||
//{
|
||||
// var transaction = self.BeginTransaction();
|
||||
// // using (var cmd = self.CreateCommand(transaction, "BEGIN IMMEDIATE;"))
|
||||
// // try { cmd.ExecuteNonQuery(); }
|
||||
// // catch {
|
||||
// // ignore
|
||||
// }
|
||||
//
|
||||
// return transaction;
|
||||
//}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a command with the given command string, and adds parameters to fit the input
|
||||
/// </summary>
|
||||
|
||||
@@ -91,11 +91,13 @@ namespace Duplicati.Library.Snapshots
|
||||
{
|
||||
if (source.IsFolder)
|
||||
{
|
||||
// Removing the null characters is a workaround for a bug in .NET core:
|
||||
// https://github.com/dotnet/runtime/issues/49803
|
||||
foreach (var f in ListFiles(source.Path))
|
||||
yield return new SnapshotSourceFileEntry(this, f, false, false);
|
||||
yield return new SnapshotSourceFileEntry(this, f.Trim('\0'), false, false);
|
||||
|
||||
foreach (var d in ListFolders(source.Path))
|
||||
yield return new SnapshotSourceFileEntry(this, Util.AppendDirSeparator(d), true, false);
|
||||
yield return new SnapshotSourceFileEntry(this, Util.AppendDirSeparator(d.Trim('\0')), true, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,9 +107,7 @@ namespace Duplicati.Library.Snapshots
|
||||
/// <param name="localPath">The full path to the file in non-snapshot format</param>
|
||||
/// <returns>The last write time of the file</returns>
|
||||
public virtual DateTime GetLastWriteTimeUtc(string localPath)
|
||||
{
|
||||
return File.GetLastWriteTimeUtc(localPath);
|
||||
}
|
||||
=> File.GetLastWriteTimeUtc(localPath);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last write time of a given file in UTC
|
||||
@@ -115,9 +115,7 @@ namespace Duplicati.Library.Snapshots
|
||||
/// <param name="localPath">The full path to the file in non-snapshot format</param>
|
||||
/// <returns>The last write time of the file</returns>
|
||||
public virtual DateTime GetCreationTimeUtc(string localPath)
|
||||
{
|
||||
return File.GetCreationTimeUtc(localPath);
|
||||
}
|
||||
=> File.GetCreationTimeUtc(localPath);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a file for reading
|
||||
@@ -125,9 +123,7 @@ namespace Duplicati.Library.Snapshots
|
||||
/// <param name="localPath">The full path to the file in non-snapshot format</param>
|
||||
/// <returns>An open filestream that can be read</returns>
|
||||
public virtual Stream OpenRead(string localPath)
|
||||
{
|
||||
return File.Open(localPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
}
|
||||
=> File.Open(localPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a file for reading
|
||||
@@ -144,9 +140,7 @@ namespace Duplicati.Library.Snapshots
|
||||
/// <param name="localPath">The full path to the file in non-snapshot format</param>
|
||||
/// <returns>The length of the file</returns>
|
||||
public virtual long GetFileSize(string localPath)
|
||||
{
|
||||
return new FileInfo(localPath).Length;
|
||||
}
|
||||
=> new FileInfo(localPath).Length;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the symlink target if the entry is a symlink, and null otherwise
|
||||
@@ -161,9 +155,7 @@ namespace Duplicati.Library.Snapshots
|
||||
/// <returns>The file attributes</returns>
|
||||
/// <param name="localPath">The file or folder to examine</param>
|
||||
public virtual FileAttributes GetAttributes(string localPath)
|
||||
{
|
||||
return File.GetAttributes(ConvertToSnapshotPath(localPath));
|
||||
}
|
||||
=> File.GetAttributes(ConvertToSnapshotPath(localPath));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata for the given file or folder
|
||||
@@ -179,9 +171,7 @@ namespace Duplicati.Library.Snapshots
|
||||
/// <returns><c>true</c> if this instance is a block device; otherwise, <c>false</c>.</returns>
|
||||
/// <param name="localPath">The file or folder to examine</param>
|
||||
public virtual bool IsBlockDevice(string localPath)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
=> false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a unique hardlink target ID
|
||||
@@ -189,9 +179,7 @@ namespace Duplicati.Library.Snapshots
|
||||
/// <returns>The hardlink ID</returns>
|
||||
/// <param name="localPath">The file or folder to examine</param>
|
||||
public virtual string HardlinkTargetID(string localPath)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
=> null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string ConvertToLocalPath(string snapshotPath);
|
||||
@@ -201,15 +189,11 @@ namespace Duplicati.Library.Snapshots
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool FileExists(string localFilePath)
|
||||
{
|
||||
return File.Exists(ConvertToSnapshotPath(localFilePath));
|
||||
}
|
||||
=> File.Exists(ConvertToSnapshotPath(localFilePath));
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool DirectoryExists(string localFolderPath)
|
||||
{
|
||||
return Directory.Exists(ConvertToSnapshotPath(localFolderPath));
|
||||
}
|
||||
=> Directory.Exists(ConvertToSnapshotPath(localFolderPath));
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -219,9 +203,7 @@ namespace Duplicati.Library.Snapshots
|
||||
/// <returns>All folders found in the folder</returns>
|
||||
/// <param name='localFolderPath'>The folder to examinate</param>
|
||||
protected virtual string[] ListFolders(string localFolderPath)
|
||||
{
|
||||
return Directory.GetDirectories(localFolderPath);
|
||||
}
|
||||
=> Directory.GetDirectories(localFolderPath);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all files in the given folder
|
||||
@@ -229,9 +211,7 @@ namespace Duplicati.Library.Snapshots
|
||||
/// <returns>All folders found in the folder</returns>
|
||||
/// <param name='localFolderPath'>The folder to examinate</param>
|
||||
protected virtual string[] ListFiles(string localFolderPath)
|
||||
{
|
||||
return Directory.GetFiles(localFolderPath);
|
||||
}
|
||||
=> Directory.GetFiles(localFolderPath);
|
||||
|
||||
#region IDisposable interface
|
||||
|
||||
|
||||
@@ -166,7 +166,10 @@ public class BackupConfigMaskingTests
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
// Suppress as we are in test code
|
||||
#pragma warning disable SYSLIB0050 // Type or member is obsolete
|
||||
var conn = (Connection)FormatterServices.GetUninitializedObject(typeof(Connection));
|
||||
#pragma warning restore SYSLIB0050 // Type or member is obsolete
|
||||
var err = conn.ValidateBackup(backup, null);
|
||||
|
||||
if (string.IsNullOrEmpty(err))
|
||||
|
||||
Reference in New Issue
Block a user