网站首页 网站源码
website
站点相关全部源代码,隐藏了一些关于服务器的信息
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");
                }
            }
        }
    }
}
loading