Exemple #1
0
    // 正引き v4/v6 Dual Stack 対応
    public async Task <List <IPAddress> > GetIpAddressListDualStackAsync(string hostname, bool preferV6 = false, CancellationToken cancel = default, bool noCache = false)
    {
        try
        {
            ConcurrentBag <Tuple <IPAddress, int> > list = new();

            var queryTypeList = new DnsResolverQueryType[] { DnsResolverQueryType.A, DnsResolverQueryType.AAAA };

            await TaskUtil.ForEachAsync(int.MaxValue, queryTypeList, async (item, index, cancel) =>
            {
                var resultsList = await GetIpAddressListSingleStackAsync(hostname, item, null, cancel, noCache);

                if (resultsList != null)
                {
                    int i = 0;
                    foreach (var ip in resultsList.Distinct(IpComparer.Comparer))
                    {
                        list.Add(new Tuple <IPAddress, int>(ip, i));

                        i++;
                    }
                }
            }, cancel, 0);

            return(list.OrderBy(x => (int)x.Item1.AddressFamily * (preferV6 ? -1 : 1)).Select(x => x.Item1).Distinct().ToList());
        }
        catch
        {
            return(new List <IPAddress>(0));
        }
    }
    // 1 つのディレクトリをバックアップする
    public async Task DoSingleDirBackupAsync(string srcDir, string destDir, CancellationToken cancel = default, string?ignoreDirNames = null)
    {
        DateTimeOffset now = DateTimeOffset.Now;

        FileSystemEntity[]? srcDirEnum = null;

        string[] ignoreDirNamesList = ignoreDirNames._NonNull()._Split(StringSplitOptions.RemoveEmptyEntries, ",", ";");

        FileMetadata?srcDirMetadata = null;

        bool noError = true;

        try
        {
            if (srcDir._IsSamei(destDir))
            {
                throw new CoresException($"srcDir == destDir. Directory path: '{srcDir}'");
            }

            srcDirMetadata = await Fs.GetDirectoryMetadataAsync(srcDir, cancel : cancel);

            srcDirEnum = (await Fs.EnumDirectoryAsync(srcDir, false, EnumDirectoryFlags.NoGetPhysicalSize, cancel)).OrderBy(x => x.Name, StrComparer.IgnoreCaseComparer).ToArray();

            FileSystemEntity[] destDirEnum = new FileSystemEntity[0];

            DirSuperBackupMetadata?destDirOldMetaData = null;

            DirSuperBackupMetadata destDirNewMetaData;

            // 宛先ディレクトリがすでに存在しているかどうか検査する
            if (await Fs.IsDirectoryExistsAsync(destDir, cancel))
            {
                destDirEnum = await Fs.EnumDirectoryAsync(destDir, false, EnumDirectoryFlags.NoGetPhysicalSize, cancel);

                // 宛先ディレクトリに存在するメタデータファイルのうち最新のファイルを取得する
                destDirOldMetaData = await GetLatestMetaDataFileNameAsync(destDir, destDirEnum, cancel);
            }
            else
            {
                // 宛先ディレクトリがまだ存在していない場合は作成する
                await Fs.CreateDirectoryAsync(destDir, FileFlags.BackupMode | FileFlags.AutoCreateDirectory, cancel);
            }

            // 宛先ディレクトリの日付情報のみ属性書き込みする
            try
            {
                await Fs.SetFileMetadataAsync(destDir, srcDirMetadata.Clone(FileMetadataCopyMode.TimeAll), cancel);
            }
            catch
            {
                // 属性書き込みは失敗してもよい
            }

            // 新しいメタデータを作成する
            destDirNewMetaData             = new DirSuperBackupMetadata();
            destDirNewMetaData.FileList    = new List <DirSuperBackupMetadataFile>();
            destDirNewMetaData.TimeStamp   = now;
            destDirNewMetaData.DirMetadata = srcDirMetadata;
            destDirNewMetaData.DirList     = new List <string>();

            // 元ディレクトリに存在するサブディレクトリ名一覧をメタデータに追記する
            foreach (var subDir in srcDirEnum.Where(x => x.IsDirectory && x.IsCurrentOrParentDirectory == false))
            {
                // シンボリックリンクは無視する
                if (subDir.IsSymbolicLink == false)
                {
                    // 無視リストのいずれにも合致しない場合のみ
                    if (ignoreDirNamesList.Where(x => x._IsSamei(subDir.Name)).Any() == false)
                    {
                        destDirNewMetaData.DirList.Add(subDir.Name);
                    }
                }
            }

            // 元ディレクトリに存在するファイルを 1 つずつバックアップする
            var fileEntries = srcDirEnum.Where(x => x.IsFile);

            RefInt concurrentNum = new RefInt();

            AsyncLock SafeLock = new AsyncLock();

            await TaskUtil.ForEachAsync(Options.NumThreads, fileEntries, async (srcFile, taskIndex, cancel) =>
            {
                long?encryptedPhysicalSize = null;

                await Task.Yield();

                string destFilePath = Fs.PathParser.Combine(destDir, srcFile.Name);

                if (Options.EncryptPassword._IsNullOrZeroLen() == false)
                {
                    destFilePath += Consts.Extensions.CompressedXtsAes256;
                }

                FileMetadata?srcFileMetadata = null;

                concurrentNum.Increment();

                try
                {
                    srcFileMetadata = await Fs.GetFileMetadataAsync(srcFile.FullPath, cancel: cancel);

                    // このファイルと同一のファイル名がすでに宛先ディレクトリに物理的に存在するかどうか確認する
                    bool exists = await Fs.IsFileExistsAsync(destFilePath, cancel);

                    bool fileChangedOrNew = false;

                    if (exists)
                    {
                        // すでに宛先ディレクトリに存在する物理的なファイルのメタデータを取得する
                        FileMetadata destExistsMetadata = await Fs.GetFileMetadataAsync(destFilePath, cancel: cancel);

                        if (Options.EncryptPassword._IsNullOrZeroLen())
                        {
                            // 暗号化なし
                            // ファイルサイズを比較する
                            if (destExistsMetadata.Size != srcFile.Size)
                            {
                                // ファイルサイズが異なる
                                fileChangedOrNew = true;
                            }
                        }
                        else
                        {
                            // 暗号化あり
                            // 宛先ディレクトリのメタデータにファイル情報が存在し、そのメタデータ情報におけるファイルサイズが元ファイルと同じであり、
                            // かつそのメタデータ情報に記載されている EncryptedPhysicalSize が宛先ディレクトリにある物理ファイルと全く同一である
                            // 場合は、宛先ファイルが正しく存在すると仮定する

                            string tmp1 = Fs.PathParser.GetFileName(destFilePath);
                            if ((destDirOldMetaData?.FileList.Where(x => x.EncrypedFileName._IsSamei(tmp1) && x.EncryptedPhysicalSize == destExistsMetadata.Size && x.MetaData.Size == srcFile.Size).Any() ?? false) == false)
                            {
                                // ファイルサイズが異なるとみなす
                                fileChangedOrNew = true;
                            }
                        }

                        // 日付を比較する。ただし宛先ディレクトリの物理的なファイルの日付は信用できないので、メタデータ上のファイルサイズと比較する
                        if (destDirOldMetaData != null)
                        {
                            DirSuperBackupMetadataFile?existsFileMetadataFromDirMetadata = destDirOldMetaData.FileList.Where(x => x.FileName._IsSamei(srcFile.Name)).SingleOrDefault();
                            if (existsFileMetadataFromDirMetadata == null)
                            {
                                // メタデータ上に存在しない
                                fileChangedOrNew = true;
                            }
                            else
                            {
                                if (existsFileMetadataFromDirMetadata.MetaData !.LastWriteTime !.Value.Ticks != srcFileMetadata.LastWriteTime !.Value.Ticks)
                                {
                                    // 最終更新日時が異なる
                                    fileChangedOrNew = true;
                                }
                            }
                        }
                        else
                        {
                            // 宛先ディレクトリ上にメタデータがない
                            fileChangedOrNew = true;
                        }
                    }
                    else
                    {
                        // 新しいファイルである
                        fileChangedOrNew = true;
                    }

                    string?oldFilePathToDelete = null;

                    if (fileChangedOrNew)
                    {
                        // ファイルが新しいか、または更新された場合は、そのファイルをバックアップする
                        // ただし、バックアップ先に同名のファイルがすでに存在する場合は、
                        // .old.xxxx.YYYYMMDD_HHMMSS.0123._backup_history のような形式でまだ存在しない連番に古いファイル名をリネームする

                        if (exists)
                        {
                            using (await SafeLock.LockWithAwait(cancel))
                            {
                                string yymmdd = Str.DateTimeToStrShort(DateTime.UtcNow);
                                string newOldFileName;

                                // 連番でかつ存在していないファイル名を決定する
                                for (int i = 0; ; i++)
                                {
                                    string newOldFileNameCandidate = $".old.{Fs.PathParser.GetFileName(destFilePath)}.{yymmdd}.{i:D4}{Consts.Extensions.DirSuperBackupHistory}";

                                    if (srcDirEnum.Where(x => x.Name._IsSamei(newOldFileNameCandidate)).Any() == false)
                                    {
                                        if (await Fs.IsFileExistsAsync(Fs.PathParser.Combine(destDir, newOldFileNameCandidate), cancel) == false)
                                        {
                                            newOldFileName = newOldFileNameCandidate;
                                            break;
                                        }
                                    }
                                }

                                // 変更されたファイル名を ._backup_history ファイルにリネーム実行する
                                string newOldFilePath = Fs.PathParser.Combine(destDir, newOldFileName);
                                await WriteLogAsync(DirSuperBackupLogType.Info, Str.CombineStringArrayForCsv("FileRename", destFilePath, newOldFilePath));
                                await Fs.MoveFileAsync(destFilePath, newOldFilePath, cancel);

                                oldFilePathToDelete = newOldFilePath;

                                // 隠しファイルにする
                                try
                                {
                                    var meta = await Fs.GetFileMetadataAsync(newOldFilePath, cancel: cancel);
                                    if (meta.Attributes != null && meta.Attributes.Bit(FileAttributes.Hidden) == false)
                                    {
                                        FileMetadata meta2 = new FileMetadata(attributes: meta.Attributes.BitAdd(FileAttributes.Hidden));

                                        await Fs.SetFileMetadataAsync(newOldFilePath, meta2, cancel);
                                    }
                                }
                                catch { }
                            }
                        }

                        // ファイルをコピーする
                        // 属性は、ファイルの日付情報のみコピーする
                        await WriteLogAsync(DirSuperBackupLogType.Info, Str.CombineStringArrayForCsv("FileCopy", srcFile.FullPath, destFilePath));

                        FileFlags flags = FileFlags.BackupMode | FileFlags.Async;

                        if (this.Options.Flags.Bit(DirSuperBackupFlags.BackupNoVerify) == false)
                        {
                            flags |= FileFlags.CopyFile_Verify;
                        }

                        await Fs.CopyFileAsync(srcFile.FullPath, destFilePath,
                                               new CopyFileParams(flags: flags, metadataCopier: new FileMetadataCopier(FileMetadataCopyMode.TimeAll),
                                                                  encryptOption: Options.EncryptPassword._IsNullOrZeroLen() ? EncryptOption.None : EncryptOption.Encrypt | EncryptOption.Compress,
                                                                  encryptPassword: Options.EncryptPassword, deleteFileIfVerifyFailed: true),
                                               cancel: cancel);

                        try
                        {
                            if (Options.EncryptPassword._IsNullOrZeroLen() == false)
                            {
                                var newFileMetadata = await Fs.GetFileMetadataAsync(destFilePath, FileMetadataGetFlags.NoPhysicalFileSize, cancel);

                                encryptedPhysicalSize = newFileMetadata.Size;
                            }
                        }
                        catch
                        {
                        }

                        if (Options.Flags.Bit(DirSuperBackupFlags.BackupMakeHistory) == false)
                        {
                            // History を残さない場合
                            // コピーに成功したので ._backup_history ファイルは削除する
                            if (oldFilePathToDelete._IsNotZeroLen())
                            {
                                try
                                {
                                    await Fs.DeleteFileAsync(oldFilePathToDelete, flags: FileFlags.BackupMode | FileFlags.ForceClearReadOnlyOrHiddenBitsOnNeed, cancel);
                                }
                                catch
                                {
                                }
                            }
                        }

                        Stat.Copy_NumFiles++;
                        Stat.Copy_TotalSize += srcFile.Size;
                    }
                    else
                    {
                        if (Options.EncryptPassword._IsNullOrZeroLen() == false)
                        {
                            string tmp1 = Fs.PathParser.GetFileName(destFilePath);

                            encryptedPhysicalSize = destDirOldMetaData?.FileList.Where(x => x.EncrypedFileName._IsSame(tmp1) && x.MetaData.Size == srcFile.Size).FirstOrDefault()?.EncryptedPhysicalSize;

                            if (encryptedPhysicalSize.HasValue == false)
                            {
                                encryptedPhysicalSize = destDirOldMetaData?.FileList.Where(x => x.EncrypedFileName._IsSamei(tmp1) && x.MetaData.Size == srcFile.Size).FirstOrDefault()?.EncryptedPhysicalSize;
                            }
                        }

                        Stat.Skip_NumFiles++;
                        Stat.Skip_TotalSize += srcFile.Size;

                        // ファイルの日付情報のみ更新する
                        try
                        {
                            await Fs.SetFileMetadataAsync(destFilePath, srcFileMetadata.Clone(FileMetadataCopyMode.TimeAll), cancel);
                        }
                        catch
                        {
                            // ファイルの日付情報の更新は失敗してもよい
                        }
                    }
                }
                catch (Exception ex)
                {
                    Stat.Error_NumFiles++;
                    Stat.Error_TotalSize += srcFile.Size;

                    // ファイル単位のエラー発生
                    await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("FileError", srcFile.FullPath, destFilePath, ex.Message));

                    noError = false;
                }
                finally
                {
                    concurrentNum.Decrement();
                }

                // このファイルに関するメタデータを追加する
                if (srcFileMetadata != null)
                {
                    lock (destDirNewMetaData.FileList)
                    {
                        destDirNewMetaData.FileList.Add(new DirSuperBackupMetadataFile()
                        {
                            FileName              = srcFile.Name,
                            EncrypedFileName      = Options.EncryptPassword._IsNullOrZeroLen() ? null : srcFile.Name + Consts.Extensions.CompressedXtsAes256,
                            MetaData              = srcFileMetadata,
                            EncryptedPhysicalSize = encryptedPhysicalSize,
                        });
                    }
                }
            }, cancel : cancel);

            // 新しいメタデータをファイル名でソートする
            destDirNewMetaData.FileList = destDirNewMetaData.FileList.OrderBy(x => x.FileName, StrComparer.IgnoreCaseComparer).ToList();
            destDirNewMetaData.DirList  = destDirNewMetaData.DirList.OrderBy(x => x, StrComparer.IgnoreCaseComparer).ToList();

            // 新しいメタデータを書き込む
            string newMetadataFilePath = Fs.PathParser.Combine(destDir, $"{PrefixMetadata}{Str.DateTimeToStrShortWithMilliSecs(now.UtcDateTime)}{SuffixMetadata}");

            await Fs.WriteJsonToFileAsync(newMetadataFilePath, destDirNewMetaData, FileFlags.BackupMode | FileFlags.OnCreateSetCompressionFlag, cancel : cancel);
        }
        catch (Exception ex)
        {
            Stat.Error_Dir++;

            noError = false;

            // ディレクトリ単位のエラー発生
            await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("DirError1", srcDir, destDir, ex.Message));
        }

        // 再度 宛先ディレクトリの日付情報のみ属性書き込みする (Linux の場合、中のファイルを更新するとディレクトリの日時が変ってしまうため)
        try
        {
            if (srcDirMetadata != null)
            {
                await Fs.SetFileMetadataAsync(destDir, srcDirMetadata.Clone(FileMetadataCopyMode.TimeAll), cancel);
            }
        }
        catch
        {
            // 属性書き込みは失敗してもよい
        }

        if (srcDirEnum != null)
        {
            bool ok = false;

            try
            {
                // ソースディレクトリの列挙に成功した場合は、サブディレクトリに対して再帰的に実行する
                foreach (var subDir in srcDirEnum.Where(x => x.IsDirectory && x.IsCurrentOrParentDirectory == false))
                {
                    // シンボリックリンクは無視する
                    if (subDir.IsSymbolicLink == false)
                    {
                        // 無視リストのいずれにも合致しない場合のみ
                        if (ignoreDirNamesList.Where(x => x._IsSamei(subDir.Name)).Any() == false)
                        {
                            await DoSingleDirBackupAsync(Fs.PathParser.Combine(srcDir, subDir.Name), Fs.PathParser.Combine(destDir, subDir.Name), cancel, ignoreDirNames);
                        }
                    }
                }

                ok = true;
            }
            catch (Exception ex)
            {
                // 何らかのディレクトリ単位のエラーで catch されていないものが発生
                Stat.Error_Dir++;

                // ディレクトリ単位のエラー発生
                await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("DirError2", srcDir, destDir, ex.Message));
            }

            if (ok)
            {
                if (noError)
                {
                    // ここまでの処理で何も問題がなければ (このディレクトリ内のすべてのファイルのコピーやメタデータの更新に成功しているなであれば)
                    // Sync オプションが付与されている場合、不要なサブディレクトリとファイルを削除する

                    if (this.Options.Flags.Bit(DirSuperBackupFlags.BackupSync))
                    {
                        try
                        {
                            // 両方のディレクトリを再列挙いたします
                            var srcDirEnum2  = (await Fs.EnumDirectoryAsync(srcDir, false, EnumDirectoryFlags.NoGetPhysicalSize, cancel)).OrderBy(x => x.Name, StrComparer.IgnoreCaseComparer).ToArray();
                            var destDirEnum2 = (await Fs.EnumDirectoryAsync(destDir, false, EnumDirectoryFlags.NoGetPhysicalSize, cancel)).OrderBy(x => x.Name, StrComparer.IgnoreCaseComparer).ToArray();

                            // 余分なファイルを削除いたします
                            var extraFiles = destDirEnum2.Where(x => x.IsFile && x.IsSymbolicLink == false)
                                             .Where(x => x.Name._StartWithi(DirSuperBackup.PrefixMetadata) == false && x.Name._EndsWithi(DirSuperBackup.SuffixMetadata) == false)
                                             .Where(x => srcDirEnum2.Where(y => y.IsFile && y.Name._IsSameiTrim(x.Name)).Any() == false)
                                             .Where(x => srcDirEnum2.Where(y => y.IsFile && (y.Name + Consts.Extensions.CompressedXtsAes256)._IsSameiTrim(x.Name)).Any() == false);

                            foreach (var extraFile in extraFiles)
                            {
                                string fullPath = Fs.PathParser.Combine(destDir, extraFile.Name);

                                await WriteLogAsync(DirSuperBackupLogType.Info, Str.CombineStringArrayForCsv("DirSyncDeleteFile", fullPath));

                                try
                                {
                                    await Fs.DeleteFileAsync(fullPath, FileFlags.BackupMode | FileFlags.ForceClearReadOnlyOrHiddenBitsOnNeed, cancel);

                                    Stat.SyncDelete_NumFiles++;
                                }
                                catch (Exception ex)
                                {
                                    Stat.Error_NumDeleteFiles++;
                                    await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("DirSyncDeleteFileError", fullPath, ex.Message));
                                }
                            }

                            // 余分なサブディレクトリを削除いたします
                            var extraSubDirs = destDirEnum2.Where(x => x.IsDirectory && x.IsCurrentOrParentDirectory == false && x.IsSymbolicLink == false)
                                               .Where(x => srcDirEnum2.Where(y => y.IsDirectory && y.IsCurrentOrParentDirectory == false && y.Name._IsSameiTrim(x.Name)).Any() == false);

                            foreach (var extraSubDir in extraSubDirs)
                            {
                                string fullPath = Fs.PathParser.Combine(destDir, extraSubDir.Name);

                                await WriteLogAsync(DirSuperBackupLogType.Info, Str.CombineStringArrayForCsv("DirSyncDeleteSubDir", fullPath));

                                try
                                {
                                    await Fs.DeleteDirectoryAsync(fullPath, true, cancel);

                                    Stat.SyncDelete_NumDirs++;
                                }
                                catch (Exception ex)
                                {
                                    Stat.Error_NumDeleteDirs++;
                                    await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("DirSyncDeleteSubDirError", fullPath, ex.Message));
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            // 何らかのディレクトリ単位のエラーで catch されていないものが発生
                            Stat.Error_Dir++;

                            // ディレクトリ単位のエラー発生
                            await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("DirSyncEnumError", srcDir, destDir, ex.Message));
                        }
                    }
                }
            }
        }


        // 再度 宛先ディレクトリの日付情報のみ属性書き込みする (Linux の場合、中のファイルを更新するとディレクトリの日時が変ってしまうため)
        try
        {
            if (srcDirMetadata != null)
            {
                await Fs.SetFileMetadataAsync(destDir, srcDirMetadata.Clone(FileMetadataCopyMode.TimeAll), cancel);
            }
        }
        catch
        {
            // 属性書き込みは失敗してもよい
        }
    }
    // 1 つのディレクトリを復元する
    public async Task DoSingleDirRestoreAsync(string srcDir, string destDir, CancellationToken cancel = default, string?ignoreDirNames = null)
    {
        DateTimeOffset now = DateTimeOffset.Now;

        FileSystemEntity[]? srcDirEnum = null;

        string[] ignoreDirNamesList = ignoreDirNames._NonNull()._Split(StringSplitOptions.RemoveEmptyEntries, ",", ";");

        try
        {
            if (srcDir._IsSamei(destDir))
            {
                throw new CoresException($"srcDir == destDir. Directory path: '{srcDir}'");
            }

            // 元ディレクトリが存在していることを確認する
            if (await Fs.IsDirectoryExistsAsync(srcDir, cancel) == false)
            {
                throw new CoresException($"The directory '{srcDir}' not found.");
            }

            // 元ディレクトリを列挙する
            srcDirEnum = (await Fs.EnumDirectoryAsync(srcDir, false, EnumDirectoryFlags.NoGetPhysicalSize, cancel)).OrderBy(x => x.Name, StrComparer.IgnoreCaseComparer).ToArray();

            // 元ディレクトリに存在するメタデータファイルのうち最新のファイルを取得する
            // なお、メタデータファイルのパースがエラーになったら、必ずエラーを発生し中断する
            DirSuperBackupMetadata?dirMetaData = await GetLatestMetaDataFileNameAsync(srcDir, srcDirEnum, cancel, throwJsonParseError : true) !;

            if (dirMetaData == null)
            {
                throw new CoresException($"Metadata not found on the directory '{srcDir}'.");
            }

            // 先ディレクトリがまだ存在していない場合は作成をする
            if (await Fs.IsDirectoryExistsAsync(destDir, cancel) == false)
            {
                FileFlags newDirFlags = FileFlags.None;

                // ディレクトリの圧縮フラグを適切に設定する
                if (dirMetaData.DirMetadata.SpecialOperationFlags.Bit(FileSpecialOperationFlags.SetCompressionFlag) || (dirMetaData.DirMetadata.Attributes?.Bit(FileAttributes.Compressed) ?? false))
                {
                    newDirFlags |= FileFlags.OnCreateSetCompressionFlag;
                }
                else
                {
                    newDirFlags |= FileFlags.OnCreateRemoveCompressionFlag;
                }

                try
                {
                    await Fs.CreateDirectoryAsync(destDir, newDirFlags, cancel);
                }
                catch
                {
                    // ヘンな圧縮フラグの設定に失敗した場合もう一度作成試行する
                    try
                    {
                        await Fs.CreateDirectoryAsync(destDir, FileFlags.None, cancel);
                    }
                    catch
                    {
                        // ディレクトリ作成コマンドでなぜかエラーになっても、結果としてディレクトリが作成されればそれでよい
                        if (await Fs.IsDirectoryExistsAsync(destDir, cancel) == false)
                        {
                            throw;
                        }

                        // やはりディレクトリの作成に失敗したらここでエラーを発生させる
                    }
                }
            }

            // ディレクトリの属性を設定する
            try
            {
                var newDirMetadata = dirMetaData.DirMetadata;

                if (Options.Flags.Bit(DirSuperBackupFlags.RestoreNoAcl))
                {
                    newDirMetadata.Security = null;
                }

                await Fs.SetDirectoryMetadataAsync(destDir, newDirMetadata, cancel);
            }
            catch (Exception ex)
            {
                // ディレクトリの属性の設定に失敗したが、軽微なエラーなのでエラーを出して続行する
                await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("DirAttributeSetError", srcDir, destDir, ex.Message));

                Stat.Error_Dir++;
            }

            // 元ディレクトリに存在するはずのファイル (メタデータに書いてある) を 1 つずつ復元する
            RefInt concurrentNum = new RefInt();

            AsyncLock SafeLock = new AsyncLock();

            await TaskUtil.ForEachAsync(Options.NumThreads, dirMetaData.FileList, async (srcFile, taskIndex, cancel) =>
            {
                await Task.Yield();

                string srcFilePath           = Fs.PathParser.Combine(srcDir, srcFile.FileName);
                string destFilePath          = Fs.PathParser.Combine(destDir, srcFile.FileName);
                FileMetadata?srcFileMetadata = null;

                concurrentNum.Increment();

                try
                {
                    bool isEncrypted       = false;
                    string encryptPassword = "";

                    if (srcFile.EncrypedFileName._IsNullOrZeroLen() == false)
                    {
                        srcFilePath = Fs.PathParser.Combine(srcDir, srcFile.EncrypedFileName);

                        // 暗号化ファイルである
                        if (Options.EncryptPassword._IsNullOrZeroLen())
                        {
                            // パスワードが指定されていない
                            throw new CoresException($"The file '{srcFilePath}' is encrypted, but no password is specified.");
                        }

                        isEncrypted     = true;
                        encryptPassword = this.Options.EncryptPassword;
                    }

                    srcFileMetadata = srcFile.MetaData;

                    // このファイルと同一の先ファイル名がすでに宛先ディレクトリに物理的に存在するかどうか確認する
                    bool exists = await Fs.IsFileExistsAsync(destFilePath, cancel);

                    bool restoreThisFile = false;

                    if (exists)
                    {
                        // すでに宛先ディレクトリに存在する物理的なファイルのメタデータを取得する
                        FileMetadata destExistsMetadata = await Fs.GetFileMetadataAsync(destFilePath, cancel: cancel);

                        if (Options.Flags.Bit(DirSuperBackupFlags.RestoreOnlyNewer) == false)
                        {
                            // 古いファイルも復元する
                            if (Options.Flags.Bit(DirSuperBackupFlags.RestoreDoNotSkipExactSame))
                            {
                                // 必ず上書きする
                                restoreThisFile = true;
                            }
                            else
                            {
                                // ファイルサイズを比較する
                                if (destExistsMetadata.Size != srcFileMetadata.Size)
                                {
                                    // ファイルサイズが異なる
                                    restoreThisFile = true;
                                }

                                // 日付を比較する
                                if (srcFileMetadata !.LastWriteTime !.Value.Ticks != destExistsMetadata.LastWriteTime !.Value.Ticks)
                                {
                                    // 最終更新日時が異なる
                                    restoreThisFile = true;
                                }

                                if (restoreThisFile == false)
                                {
                                    // 新旧両方のファイルが存在する場合で、ファイルサイズも日付も同じであれば、復元先ファイル内容がバックアップファイルと同一かチェックし、同一の場合はコピーをスキップする
                                    ResultOrError <int> sameRet;

                                    if (isEncrypted == false)
                                    {
                                        sameRet = await FileUtil.CompareFileHashAsync(new FilePath(srcFilePath, Fs, flags: FileFlags.BackupMode), new FilePath(destFilePath, Fs, flags: FileFlags.BackupMode), cancel: cancel);
                                    }
                                    else
                                    {
                                        sameRet = await FileUtil.CompareEncryptedFileHashAsync(encryptPassword, true, new FilePath(destFilePath, Fs, flags: FileFlags.BackupMode), new FilePath(srcFilePath, Fs, flags: FileFlags.BackupMode), cancel: cancel);
                                    }

                                    if (sameRet.IsOk == false || sameRet.Value != 0)
                                    {
                                        restoreThisFile = true;
                                    }
                                }
                            }
                        }
                        else
                        {
                            // バックアップのほうが新しい場合のみ復元するモード
                            // 日付を比較する
                            if (srcFileMetadata !.LastWriteTime !.Value.Ticks > destExistsMetadata.LastWriteTime !.Value.Ticks)
                            {
                                // 最終更新日時がバックアップのほうが新しい
                                restoreThisFile = true;
                            }
                        }
                    }
                    else
                    {
                        restoreThisFile = true;
                    }

                    if (restoreThisFile)
                    {
                        // すべての判断に合格したら、このファイルの復元を実施する
                        if (exists)
                        {
                            if (Options.Flags.Bit(DirSuperBackupFlags.RestoreMakeBackup))
                            {
                                // 復元先に同名のファイルがすでに存在する場合は、
                                // .original.xxxx.0123.original のような形式でまだ存在しない連番に古いファイル名をリネームする
                                using (await SafeLock.LockWithAwait(cancel))
                                {
                                    string newOldFileName;

                                    // 連番でかつ存在していないファイル名を決定する
                                    for (int i = 0; ; i++)
                                    {
                                        string newOldFileNameCandidate = $".original.{srcFile.FileName}.{i:D4}.original";

                                        if (await Fs.IsFileExistsAsync(Fs.PathParser.Combine(destDir, newOldFileNameCandidate), cancel) == false)
                                        {
                                            newOldFileName = newOldFileNameCandidate;
                                            break;
                                        }
                                    }

                                    // 変更されたファイル名を .old ファイルにリネーム実行する
                                    string newOldFilePath = Fs.PathParser.Combine(destDir, newOldFileName);
                                    await WriteLogAsync(DirSuperBackupLogType.Info, Str.CombineStringArrayForCsv("FileRename", destFilePath, newOldFilePath));
                                    await Fs.MoveFileAsync(destFilePath, newOldFilePath, cancel);

                                    // 隠しファイルにする
                                    try
                                    {
                                        var meta = await Fs.GetFileMetadataAsync(newOldFilePath, cancel: cancel);
                                        if (meta.Attributes != null)
                                        {
                                            FileMetadata meta2 = new FileMetadata(attributes: meta.Attributes?.BitAdd(FileAttributes.Hidden));

                                            await Fs.SetFileMetadataAsync(newOldFilePath, meta2, cancel);
                                        }
                                    }
                                    catch { }
                                }
                            }
                        }

                        // 復元メイン
                        await WriteLogAsync(DirSuperBackupLogType.Info, Str.CombineStringArrayForCsv("FileCopy", srcFilePath, destFilePath));

                        FileFlags flags = FileFlags.BackupMode | FileFlags.Async;

                        if (this.Options.Flags.Bit(DirSuperBackupFlags.RestoreNoVerify) == false)
                        {
                            flags |= FileFlags.CopyFile_Verify;
                        }

                        // ファイルをコピーする
                        await Fs.CopyFileAsync(srcFilePath, destFilePath,
                                               new CopyFileParams(flags: flags,
                                                                  metadataCopier: new FileMetadataCopier(FileMetadataCopyMode.TimeAll),
                                                                  encryptOption: isEncrypted ? EncryptOption.Decrypt | EncryptOption.Compress : EncryptOption.None,
                                                                  encryptPassword: encryptPassword),
                                               cancel: cancel,
                                               newFileMeatadata: Options.Flags.Bit(DirSuperBackupFlags.RestoreNoAcl) ? null : srcFileMetadata);

                        Stat.Copy_NumFiles++;
                        Stat.Copy_TotalSize += srcFile.MetaData.Size;

                        // メタデータを再度復元
                        try
                        {
                            var meta = Options.Flags.Bit(DirSuperBackupFlags.RestoreNoAcl) ? srcFileMetadata.Clone(FileMetadataCopyMode.TimeAll | FileMetadataCopyMode.Attributes | FileMetadataCopyMode.ReplicateArchiveBit | FileMetadataCopyMode.AlternateStream | FileMetadataCopyMode.Author) : srcFileMetadata;

                            await Fs.SetFileMetadataAsync(destFilePath, meta, cancel);
                        }
                        catch (Exception ex)
                        {
                            FileMetadata?existingFileMetadata = null;
                            try
                            {
                                existingFileMetadata = await Fs.GetFileMetadataAsync(destFilePath, cancel: cancel);
                            }
                            catch { }
                            if ((existingFileMetadata?.Attributes?.Bit(FileAttributes.ReadOnly) ?? true) == false)
                            {
                                // メタデータの属性の設定に失敗したが、軽微なエラーなのでエラーを出して続行する
                                // (宛先ファイルが ReadOnly の場合は、この操作はエラーとなる可能性が高い。このような場合は、予期されている動作なので、エラーは表示しない)
                                await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("FileAttributeSetError", srcFilePath, destFilePath, ex.Message));
                                Stat.Error_NumFiles++;
                            }
                        }
                    }
                    else
                    {
                        Stat.Skip_NumFiles++;
                        Stat.Skip_TotalSize += srcFile.MetaData.Size;
                    }
                }
                catch (Exception ex)
                {
                    Stat.Error_NumFiles++;
                    Stat.Error_TotalSize += srcFileMetadata?.Size ?? 0;

                    // ファイル単位のエラー発生
                    await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("FileError", srcFilePath, destFilePath, ex.Message));
                }
                finally
                {
                    concurrentNum.Decrement();
                }
            });

            // このディレクトリの全ファイルの復元が終わったら、ディレクトリのタイムスタンプ情報を再書き込みする
            // (中のファイルが新しくなったことが原因で、ディレクトリの更新日時が新しくなってしまう可能性があるためである)
            try
            {
                await Fs.SetFileMetadataAsync(destDir, dirMetaData.DirMetadata.Clone(FileMetadataCopyMode.TimeAll), cancel);
            }
            catch
            {
                // 属性書き込みは失敗してもよい
            }

            try
            {
                // ソースディレクトリの列挙に成功した場合は、サブディレクトリに対して再帰的に実行する
                foreach (var subDir in dirMetaData.DirList)
                {
                    // 無視リストのいずれにも合致しない場合のみ
                    if (ignoreDirNamesList.Where(x => x._IsSamei(subDir)).Any() == false)
                    {
                        await DoSingleDirRestoreAsync(Fs.PathParser.Combine(srcDir, subDir), Fs.PathParser.Combine(destDir, subDir), cancel, ignoreDirNames);
                    }
                }
            }
            catch (Exception ex)
            {
                // 何らかのディレクトリ単位のエラーで catch されていないものが発生
                Stat.Error_Dir++;

                // ディレクトリ単位のエラー発生
                await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("DirError1", srcDir, destDir, ex.Message));
            }

            // 再度 宛先ディレクトリの日付情報のみ属性書き込みする (Linux の場合、中のファイルを更新するとディレクトリの日時が変ってしまうため)
            try
            {
                if (dirMetaData != null)
                {
                    await Fs.SetFileMetadataAsync(destDir, dirMetaData.DirMetadata.Clone(FileMetadataCopyMode.TimeAll), cancel);
                }
            }
            catch
            {
                // 属性書き込みは失敗してもよい
            }
        }
        catch (Exception ex)
        {
            Stat.Error_Dir++;

            // ディレクトリ単位のエラー発生
            await WriteLogAsync(DirSuperBackupLogType.Error, Str.CombineStringArrayForCsv("DirError2", srcDir, destDir, ex.Message));
        }
    }
    // Git リポジトリの自動ダウンロード
    async Task Loop2_MainteUsersAsync(CancellationToken cancel = default)
    {
        long lastHookTick = -1;

        while (cancel.IsCancellationRequested == false)
        {
            try
            {
                await Lfs.CreateDirectoryAsync(this.Settings.GitMirrorDataRootDir);

                await Lfs.CreateDirectoryAsync(this.Settings.GitWebDataRootDir);
            }
            catch
            {
            }

            try
            {
                // Git リポジトリを列挙
                var projects = await this.GitLabClient.EnumProjectsAsync(cancel);

                ConcurrentHashSet <string> dirNames = new ConcurrentHashSet <string>(StrCmpi);

                // メモ: last_activity_at の値を信用してはならない。これは GitLab がキャッシュしているので、なかなか更新されない。

                var targetProjects = projects.Where(p => p.empty_repo == false && p.path_with_namespace._IsFilled() && p.default_branch._IsFilled() && p.visibility._IsDiffi("private")).OrderByDescending(x => x.last_activity_at).ThenBy(x => x.path_with_namespace, StrCmpi);

                await TaskUtil.ForEachAsync(8, targetProjects, async (proj, index, cancel) =>
                {
                    cancel.ThrowIfCancellationRequested();

                    try
                    {
                        string dirname = proj.GenerateDirName();

                        dirNames.Add(dirname);

                        string gitRoot = this.Settings.GitMirrorDataRootDir._CombinePath(dirname);
                        string webRoot = this.Settings.GitWebDataRootDir._CombinePath(dirname);

                        await this.GitLabClient.GitPullFromRepositoryAsync(proj.path_with_namespace !, gitRoot, proj.default_branch !, cancel);

                        await this.SyncGitLocalRepositoryDirToWebRootDirAsync(gitRoot, webRoot, cancel);
                    }
                    catch (Exception ex)
                    {
                        ex._Error();
                    }
                }, cancel);

                // GitLab 上に存在せず local に存在する gitRoot を列挙して削除する
                var existingLocalGitDirs = await Lfs.EnumDirectoryAsync(this.Settings.GitMirrorDataRootDir, cancel : cancel);

                foreach (var d in existingLocalGitDirs.Where(x => x.IsDirectory && dirNames.Contains(x.Name) == false))
                {
                    cancel.ThrowIfCancellationRequested();

                    try
                    {
                        await Lfs.DeleteDirectoryAsync(d.FullPath, true, cancel, true);
                    }
                    catch { }
                }

                // GitLab 上に存在せず local に存在する webRoot を列挙して削除する
                var existingLocalWebDirs = await Lfs.EnumDirectoryAsync(this.Settings.GitWebDataRootDir, cancel : cancel);

                foreach (var d in existingLocalWebDirs.Where(x => x.IsDirectory && dirNames.Contains(x.Name) == false))
                {
                    cancel.ThrowIfCancellationRequested();

                    try
                    {
                        await Lfs.DeleteFileAsync(d.FullPath._CombinePath(Consts.FileNames.LogBrowserSecureJson), cancel : cancel);
                    }
                    catch { }
                }
            }
            catch (Exception ex)
            {
                ex._Error();
            }

            await TaskUtil.AwaitWithPollAsync(this.Settings.ForceRepositoryUpdateIntervalMsecs, 500, () =>
            {
                long currentHookTick = this.HookFiredTick;

                if (lastHookTick != currentHookTick)
                {
                    lastHookTick = currentHookTick;
                    return(true);
                }

                return(false);
            },
                                              cancel,
                                              true);
        }
    }