using System.Linq.Dynamic.Core;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Dpz.Core.Infrastructure;
using Dpz.Core.Infrastructure.Entity;
using Dpz.Core.MongodbAccess;
using Dpz.Core.Public.ViewModel;
using FluentFTP;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver.Linq;
namespace Dpz.Core.Backup;
public class BackupRestore : IBackupRestore
{
private readonly ILogger<BackupRestore> _logger;
private readonly IConfiguration _configuration;
public BackupRestore(
ILogger<BackupRestore> logger,
IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
private static List<BackupType> GetAllBackupCollection()
{
return Assembly.Load("Dpz.Core.Public.Entity").GetExportedTypes().Select(x =>
{
var attribute = x.GetCustomAttribute<BackupAttribute>();
if (attribute != null) // && typeof(IBaseEntity).IsAssignableFrom(x)
{
return new BackupType
{
Type = x,
Attribute = attribute
};
}
return null;
}).Where(x => x != null).Select(x => x!).ToList();
}
private async Task<Dictionary<string, object>> GetBackupDataAsync(string? connectionString)
{
var dic = new Dictionary<string, object>();
var types = GetAllBackupCollection();
foreach (var type in types)
{
if (type is { Type: null }) continue;
var repositoryType = typeof(Repository<>);
try
{
// 泛型约束
var genericArguments = repositoryType.GetGenericArguments();
if (!genericArguments.All(
x => x.GetGenericParameterConstraints().All(y => y.IsAssignableFrom(type.Type))))
{
continue;
}
// IRepository<T> 实例类型
var instanceType = repositoryType.MakeGenericType(type.Type);
// repository 实例对象
var repository = Activator.CreateInstance(instanceType, connectionString);
// repository.SearchFor(Expression<Func<T, bool>> predicate) 方法
var searchForMethod = (from x in instanceType.GetMethods()
let parameters = x.GetParameters()
where x.Name == "SearchFor" && parameters.Length == 1 && parameters[0].Name == "predicate"
select x).FirstOrDefault();
if (repository != null && searchForMethod != null)
{
var lambda =
DynamicExpressionParser.ParseLambda(
parsingConfig: ParsingConfig.Default,
createParameterCtor: false,
itType: type.Type,
resultType: null,
expression: !string.IsNullOrEmpty(type.Attribute?.Where)
? type.Attribute.Where
: "x => true");
var returnValue = searchForMethod.Invoke(repository, new object?[] { lambda }) as IMongoQueryable;
var data = new List<dynamic>();
if (returnValue != null)
{
data = await returnValue.ToDynamicListAsync();
}
if (data.Count > 0)
{
dic.TryAdd(type.Type.Name, data);
}
}
}
catch (Exception e)
{
_logger.LogError(e, "Get backup data fail,type:{@Type},Attribute:{@Where}", type.Type, type.Attribute);
}
}
return dic;
}
private string Sha256Encrypt(string input)
{
var buffer = Encoding.UTF8.GetBytes(input);
var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(buffer);
return Convert.ToBase64String(hashBytes);
}
/// <summary>
/// 创建一个zip文件
/// </summary>
/// <param name="outPathname">zip文件路径</param>
/// <param name="password">密码</param>
/// <param name="folderName">要压缩的文件夹路径</param>
private string CreateZip(string outPathname, string password, string folderName)
{
using var fsOut = File.Create(outPathname);
using var zipStream = new ZipOutputStream(fsOut);
//0-9, 9 是最高压缩级别
zipStream.SetLevel(9);
// 密码
zipStream.Password = password;
var folderOffset = folderName.Length + (folderName.EndsWith(Path.PathSeparator) ? 0 : 1);
CompressFolder(folderName, zipStream, folderOffset);
return fsOut.Name;
}
/// <summary>
/// 递归压缩文件夹
/// </summary>
/// <param name="path"></param>
/// <param name="zipStream"></param>
/// <param name="folderOffset"></param>
private void CompressFolder(string path, ZipOutputStream zipStream, int folderOffset)
{
var files = Directory.GetFiles(path);
foreach (var filename in files)
{
var fi = new FileInfo(filename);
// 根据文件夹名称命名zip文件名
var entryName = filename[folderOffset..];
// 修正不同系统造成的不同目录的斜杠问题
entryName = ZipEntry.CleanName(entryName);
var newEntry = new ZipEntry(entryName)
{
// Zip 存储2秒的颗粒度
DateTime = fi.LastWriteTime,
// 指定 AESKeySize 会使用 AES 加密。
// 允许的值为 0(关闭)、128 或 256。
// 如果使用 AES,则需要 ZipOutputStream 上的密码
AESKeySize = 256,
/*
* 为了让WinXP和Server2003中内置的解压程序、WinZip 8、Java和其他旧代码能够解压缩这个zip文件,你需要执行以下操作之一:
* 指定 UseZip64.Off 或设置文件大小。
* 如果文件大小超过4GB,或者不需要WinXP内置的兼容性,那么不需要上述操作,但是生成的zip文件将是Zip64格式,而并非所有工具都能理解这种格式。
*/
// zipStream.UseZip64 = UseZip64.Off;
Size = fi.Length
};
zipStream.PutNextEntry(newEntry);
// Zip 缓冲区
var buffer = new byte[4096];
using (var fsInput = File.OpenRead(filename))
{
StreamUtils.Copy(fsInput, zipStream, buffer);
}
zipStream.CloseEntry();
}
// 递归
var folders = Directory.GetDirectories(path);
foreach (var folder in folders)
{
CompressFolder(folder, zipStream, folderOffset);
}
}
/// <summary>
/// 解压缩zip
/// </summary>
/// <param name="archivePath">压缩文件路径</param>
/// <param name="outFolder"></param>
/// <param name="password"></param>
private void ExtractZipFile(string archivePath, string outFolder, string password)
{
using var fsInput = File.OpenRead(archivePath);
using var zf = new ZipFile(fsInput);
if (!string.IsNullOrEmpty(password))
{
zf.Password = password;
}
foreach (ZipEntry zipEntry in zf)
{
var entryFileName = zipEntry.Name;
var fullZipToPath = Path.Combine(outFolder, entryFileName);
var directoryName = Path.GetDirectoryName(fullZipToPath);
if (directoryName is { Length: > 0 })
{
Directory.CreateDirectory(directoryName);
}
var buffer = new byte[4096];
using var zipStream = zf.GetInputStream(zipEntry);
using Stream fsOutput = File.Create(fullZipToPath);
StreamUtils.Copy(zipStream, fsOutput, buffer);
}
}
private async Task UploadBackupAsync(string zipPath)
{
if (!File.Exists(zipPath))
return;
await ActionFtpAsync(async client =>
{
#if DEBUG
var remotePath = $"/Test/backup/{Path.GetFileName(zipPath)}";
#else
var remotePath = $"/backup/{Path.GetFileName(zipPath)}";
#endif
await client.UploadFile(zipPath, remotePath,
createRemoteDir: true);
return "";
});
}
/// <summary>
/// 下载最近的备份数据
/// </summary>
private async Task<string> DownloadBackupAsync()
{
#if DEBUG
var remotePath = $"/Test/backup/";
#else
var remotePath = $"/backup/";
#endif
var directoryInfo = new DirectoryInfo(Path.Combine("restore"));
if (!directoryInfo.Exists)
directoryInfo.Create();
return await ActionFtpAsync(async client =>
{
var downloadItem = (await client.GetListing(remotePath))?.OrderByDescending(x => x.Name)
.FirstOrDefault(x => x.Type == FtpObjectType.File);
var zipPath = "";
if (downloadItem != null)
{
zipPath = Path.Combine(directoryInfo.FullName, downloadItem.Name);
await client.DownloadFile(
zipPath,
downloadItem.FullName,
FtpLocalExists.Overwrite,
FtpVerify.Retry);
}
return zipPath;
});
}
private async Task<string> ActionFtpAsync(Func<AsyncFtpClient, Task<string>> handle)
{
var upyunOperator = _configuration.GetSection("upyun").Get<UpyunOperator>();
if (upyunOperator == null)
throw new BusinessException("configuration error,need upyun config node.");
const string ftpHost = "v0.ftp.upyun.com";
var str = "";
try
{
using var client = new AsyncFtpClient(ftpHost, $"{upyunOperator.Operator}/{upyunOperator.Bucket}",
upyunOperator.Password);
client.Config.ReadTimeout = 15 * 60 * 1000;
await client.Connect();
str = await handle(client);
await client.Disconnect();
}
catch (Exception ex)
{
_logger.LogError(ex, "Action ftp fail");
}
return str;
}
public async Task<VmBackupRecord> BackupAsync(string? connectionString)
{
var today = DateTime.Now.ToString("yyyyMMdd");
var backupPath = Path.Combine("backup", today);
var directoryInfo = new DirectoryInfo(backupPath);
if (!directoryInfo.Exists)
{
directoryInfo.Create();
}
var backupData = await GetBackupDataAsync(connectionString);
foreach (var item in backupData)
{
await using var stream = new FileStream(
Path.Combine(directoryInfo.FullName, item.Key + ".json"),
FileMode.Create, FileAccess.Write);
await using var writer = new StreamWriter(stream);
var settings =
#if DEBUG
new JsonWriterSettings { Indent = true };
#else
JsonWriterSettings.Defaults;
#endif
var json = item.Value.ToJson(settings) ?? "[]";
await writer.WriteLineAsync(json);
}
var password =
#if DEBUG
"123";
#else
Sha256Encrypt(_configuration["BackupKey"] + today);
#endif
var filename = today + ".zip";
var zipPath = CreateZip(Path.Combine("backup", filename), password, backupPath);
#if !DEBUG
await UploadBackupAsync(zipPath);
#endif
return new VmBackupRecord
{
BackupPassword = password,
BackupTime = DateTime.Now,
Filename = filename
};
}
public async Task RestoreAsync(string? connectionString)
{
var zipPath = await DownloadBackupAsync();
if (string.IsNullOrEmpty(zipPath) || !File.Exists(zipPath))
return;
var filename = Path.GetFileName(zipPath);
var index = filename.LastIndexOf('.');
var date = index <= 0 ? filename : filename[..index];
var password = Sha256Encrypt(_configuration["BackupKey"] + date);
// 解压缩备份文件存放文件夹
var restorePath = Path.Combine(zipPath, "..", date);
ExtractZipFile(zipPath, restorePath, password);
var types = GetAllBackupCollection();
foreach (var type in types)
{
if (type is { Type: null }) continue;
// 备份文件路径
var dataPath = Path.Combine(restorePath, $"{type.Type.Name}.json");
if (!File.Exists(dataPath)) continue;
var repositoryType = typeof(Repository<>);
var instanceType = repositoryType.MakeGenericType(type.Type);
var repository = Activator.CreateInstance(instanceType, connectionString);
var insertMethod = (from x in instanceType.GetMethods()
let parameters = x.GetParameters()
where x.Name == "InsertAsync" && parameters.Length == 1 && parameters[0].Name == "source" &&
parameters[0].ParameterType.IsInterface
select x).FirstOrDefault();
if (repository != null && insertMethod != null)
{
try
{
var listType = typeof(List<>).MakeGenericType(type.Type);
var fileStream = new FileStream(dataPath, FileMode.Open);
using var reader = new StreamReader(fileStream);
var data = BsonSerializer.Deserialize(reader, listType);
if (data != null)
{
await ((insertMethod.Invoke(repository, new[] { data }) as Task)!);
}
}
catch (Exception e)
{
_logger.LogError(e, "Restore data fail");
}
}
}
}
}