Windowsのユーザーディレクトリ配下の一時ディレクトリをクリーンアップするツール

この記事は公開から1年以上経過しています。

過去のエントリ「Windowsのサインイン(ログイン)が非常に遅いときの対応」にて、ユーザーディレクトリ配下の一時ディレクトリC:\Users\UserName\AppData\Local\Tempにファイルが溜まることでWindowsのログインに膨大な時間が掛かってしまう問題が確認されたため、当該ディレクトリを自動クリーンアップするツールを.NET 7(C#)で作ってみました。

Windows 10/11(64bit)の場合は、以下のリンクからビルド済バイナリをダウンロード可能、VirusTotalにてウイルスチェック済です。

UserTempCleaner.zip

.NET 7(AOT)でビルドしていますが、.NET 7が入っていない環境が手元にないため、.NET 7ランタイムが未インストールの環境で動作するか未検証です。

一時ディレクトリには消されても問題ないファイルを配置すべきですが、アプリケーションによっては消されることを前提としていない作りのものもあるため、削除することで問題が生じる場合があります。

本ツールを使用することによっていかなる問題や損害等について、エクシードシステムは一切の責任を負いません(At your own risk)。


サンプルソースコード(C#)

UserTempCleaner.cs

amespace UserTempCleaner
{
    internal class Program
    {
        // Threshold of elapsed time since the last use of the file/folder
        private const int ELAPSED_HOURS = 168;

        private static readonly string LOG_FILE = $"UserTempCleaner_{DateTime.Now:yyyyMMddHHmmss}.log";

        private static void Main(string[] args)
        {
            WriteLogFile("Begin:");

            var userTempDirPath = Environment.GetEnvironmentVariable("TEMP");

            if (userTempDirPath == null)
            {
                WriteLogFile($"Environment variable 'TEMP' not defined.");
                return;
            }

            var fileTree = GetFileTree(userTempDirPath);

            Cleanup(userTempDirPath, fileTree);

            WriteLogFile("End:");
        }

        private static FileTree GetFileTree(string path, FileTree? root = null)
        {
            FileTree parent = root ?? new FileTree(null, path, true);

            var atrb = File.GetAttributes(path);
            if ((atrb & FileAttributes.Directory) == FileAttributes.Directory)
            {
                if (Directory.Exists(path))
                {
                    foreach (var dir in Directory.GetDirectories(path))
                    {
                        var fileTree = new FileTree(parent, dir, true);
                        var subDirInfo = GetFileTree(dir, fileTree);
                        parent.AddChild(subDirInfo);
                    }
                }
            }

            foreach (var file in Directory.GetFiles(path))
            {
                var fileTree = new FileTree(parent, file);
                parent.AddChild(fileTree);
            }

            return parent;
        }

        private static DateTime GetLastUsedTime(string path)
        {
            var cTime = File.GetCreationTime(path);
            var wTime = File.GetLastWriteTime(path);
            var aTime = File.GetLastAccessTime(path);

            return new[] { cTime, wTime, aTime }.Max();
        }

        private static bool Cleanup(string basePath, FileTree fileTree)
        {
            var files = fileTree.Children!.Where(o => !o.IsDirectory);

            if (fileTree.Parent == null)
            {
                // When the root directory is the target

                foreach (var file in files)
                {
                    if (File.Exists(file.Path))
                    {
                        if (IsTarget(GetLastUsedTime(file.Path)) && !IsFileLocked(file.Path))
                        {
                            MoveToRecycleBin(basePath, file.Path, TargetType.File);
                        }
                    }
                }
            }
            else
            {
                // When the subdirectory is the target

                if (!files.All(o => IsTarget(GetLastUsedTime(o.Path)) && !IsFileLocked(o.Path)))
                {
                    // Exclude the directory from deletion if there's even one file that cannot be deleted
                    return false;
                }
            }

            var directories = fileTree.Children!.Where(o => o.IsDirectory);

            foreach (var directory in directories)
            {
                if (Cleanup(basePath, directory))
                {
                    if (fileTree.Parent == null)
                    {
                        if (Directory.Exists(directory.Path))
                        {
                            if (IsTarget(GetLastUsedTime(directory.Path)))
                            {
                                MoveToRecycleBin(basePath, directory.Path, TargetType.Directory);
                            }
                        }
                    }
                }
                else
                {
                    if (fileTree.Parent != null)
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        private static bool IsTarget(DateTime dtm)
        {
            var diff = DateTime.Now.Subtract(dtm);
            return (diff.TotalHours >= ELAPSED_HOURS);
        }

        private static void MoveToRecycleBin(string basePath, string path, TargetType targetType)
        {
            if (!path.StartsWith(basePath, StringComparison.OrdinalIgnoreCase))
            {
                // Path mismatch (potential for directory traversal)
                WriteLogFile($"Ignored({targetType.ToString().ToLower()}):'{path}'");
                return;
            }

            try
            {
                switch (targetType)
                {
                    case TargetType.Directory:
                        Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(
                            path,
                            Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs,
                            Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
                        break;

                    case TargetType.File:
                        Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(
                            path,
                            Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs,
                            Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
                        break;
                }
                WriteLogFile($"Deleted({targetType.ToString().ToLower()}):'{path}'");
            }
            catch (Exception e)
            {
                WriteLogFile($"Error:Could not delete {targetType.ToString().ToLower()} '{path}'. ({e.Message})");
            }
        }

        private static bool IsFileLocked(string filePath)
        {
            try
            {
                new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None).Dispose();
            }
            catch (IOException)
            {
                return true;
            }

            return false;
        }

        private static void WriteLogFile(string msg)
        {
            var logMsg = $"{DateTime.Now:yyyyMMddHHmmss}:{msg}";
            Console.WriteLine(logMsg);
            File.AppendAllLines(LOG_FILE, new[] { logMsg });
        }

        private sealed class FileTree
        {
            public List<FileTree>? _children;

            public FileTree(FileTree? parent, string path, bool isDirectory = false)
            {
                Parent = parent;
                Path = path;
                if (isDirectory)
                {
                    _children = new List<FileTree>();
                }
            }

            public FileTree? Parent { get; init; }

            public string Path { get; init; }

            public bool IsDirectory => _children != null;

            public IReadOnlyList<FileTree>? Children => _children;

            public void AddChild(FileTree fileTree) => _children?.Add(fileTree);

            public override string ToString() => $"Path={Path}, Children={Children?.Count}";
        }

        private enum TargetType
        {
            Directory,
            File
        }
    }
}

GitHub Gist


使い方

プログラムを起動するとC:\Users\UserName\AppData\Local\Tempフォルダ内にある一定時間(7日間決め打ち)以上使用されておらず現在アクセス中でないファイルとフォルダをゴミ箱へ削除します。
フォルダの場合はフォルダ内にある全てのフォルダおよびファイルが一定時間以上使用されてい場合のみ削除対象となります(安全策)。
カレントディレクトリにUserTempCleaner_YYYYMMDDHHMMSS.log形式で処理されたファイルなどをログファイルを出力します。

※管理者権限で作成されたファイルがある場合、管理者権限で実行しないとアクセスできずエラーが発生します。

一時ファイルを使用するプログラムのファイルアクセスのタイミングによってはチェックをすり抜けてファイルが削除できない旨のダイアログが表示される場合があります。そのような状態になった場合は、安全のためタスクマネージャーなどから本ツールのプロセスを強制終了して処理を中止してください。


以上です。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする