※当サイトの記事には、広告・プロモーションが含まれます。

ファイルマネージャーのGUIのようなことを真似してブラウザー上でフォルダの階層表示してみる

gigazine.net

⇧ う~む、これだけのことができるのに、MECE(Mutually Exclusive, Collectively Exhaustive)に分類したりする作業はポンコツだったりするのがよく分からんですな...

とりあえず、映像系の製作とかでPoC(Proof of Concept)ができるようになるのは良いですな。

ファイルマネージャーとは

Wikipediaさんによりますと、

file manager or file browser is a computer program that provides a user interface to manage files and folders. The most common operations performed on files or groups of files include creating, opening (e.g. viewing, playing, editing or printing), renaming, copyingmovingdeleting and searching for files, as well as modifying file attributes, properties and file permissionsFolders and files may be displayed in a hierarchical tree based on their directory structure.

https://en.wikipedia.org/wiki/File_manager

⇧ とあるのだけど、定義がかなり曖昧...

ファイルやフォルダーを管理する機能を提供するプログラムということで、何が管理できれば良いかとか特に決まってないっぽい...

てっきり、OS(Operation System)のファイルシステムのように、OS(Operation System)が提供している機能の1つかと思っていたんだけど、そういうわけでは無さそうということなんかね。

ただ、ファイルシステムには依存していると思うけども。

ファイルやフォルダにアクセスする流れとして、

developer.ibm.com

⇧ 上図のような仕組みだとすると、ファイルマネージャーは、「System call interface」を経由するということになるということですかね。

Windowsエクスプローラーのように、デフォルトで用意されているファイルマネージャーとLinuxのファイルマネージャーだと思想が異なるんかな?

いまいち、OS(Operation System)の機能とファイルマネージャーとの関係が分からん...

ちなみに、

news.mynavi.jp

CUIでもファイルマネージャーを利用できるらしいので、ファイルマネージャーの仕組み、謎ですな...

ファイルマネージャーのGUIのようなことを真似してブラウザー上でフォルダの階層表示してみる

前に、

ts0818.hatenablog.com

WinMergeみたいなことをブラウザで実現してみたいって話をしたんだけど、それっぽいもののたたき台のようなものができたので、ソースコードを載せておきます。

たたき台であっても無茶苦茶に時間かかったし、パフォーマンスも悪いし、JavaScriptもよく分からんことになってるしで、徒労感しかない...

まぁ、何となくの雰囲気だけでも共有できれば幸いですと。

■/test-web-file-system/build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.2'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '21'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}
    

■/test-web-file-system/src/main/java/com/example/demo/config/CorsConfig.java

package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE");
    }
}    

■/test-web-file-system/src/main/java/com/example/demo/dto/FileNodeDto.java

package com.example.demo.dto;

import java.util.List;

import lombok.Data;

@Data
public class FileNodeDto {

    private String name;
    private String type; // ファイルの種類を示す"type"プロパティを追加
    private long size;
    private long creationTime;
    private String fullPath;
    private String owner;
    private String group;
    private long lastModified;
    private PosixFilePermissionDto posixFilePermissionDto;
    private List children;
	
}
   

■/test-web-file-system/src/main/java/com/example/demo/dto/PosixFilePermissionDto.java

package com.example.demo.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PosixFilePermissionDto {
    private String filePermissionOfUser;
    private String filePermissionOfGroup;
    private String filePermissionOfOther;
}
    

■/test-web-file-system/src/main/java/com/example/demo/dto/FolderStatisticsDto.java

package com.example.demo.dto;

import lombok.Data;

@Data
public class FolderStatisticsDto {
    private int folderCount = 0;
    private int fileCount = 0;
    private long totalSize = 0;
    private long totalProcessTime = 0;
}    

■/test-web-file-system/src/main/java/com/example/demo/controller/FileTreeController.java

package com.example.demo.controller;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.dto.FileNodeDto;
import com.example.demo.dto.FolderStatisticsDto;
import com.example.demo.dto.PosixFilePermissionDto;

@RestController
public class FileTreeController {
    private final Map<String, FolderStatisticsDto> folderStatisticsMap = new HashMap<>();
    private static int totalFolderCount = 0;
    private static int totalFileCount = 0;
    private static long totalSize = 0;
    private static long totalProcessTime = 0;

    @GetMapping("/filetree")
    public List<FileNodeDto> getFileTree() {
        String rootPath = "C:\\Users\\Toshinobu\\Desktop\\soft_work\\javascript_work";
        File rootDirectory = new File(rootPath);
        List<FileNodeDto> fileNodeDto = buildFileTree(rootDirectory, rootDirectory.getAbsolutePath());
        displayFolderStatistics();
        displayTotalStatistics();
        return fileNodeDto;
    }

    private List<FileNodeDto> buildFileTree(File directory, String parentPath) {
        final long startTime = System.nanoTime();
        List<FileNodeDto> fileTree = new ArrayList<>();
        File[] files = directory.listFiles();
        BasicFileAttributes basicFileAttrs;
        long creationTime = 0;
        long lastModifiedTime = 0;

        if (files != null) {
            long folderSize = 0;
            for (File file : files) {
                try {
                    basicFileAttrs = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
                    FileTime attrsCreatedTime = basicFileAttrs.creationTime();
                    FileTime attrsLastModifiedTime = basicFileAttrs.lastModifiedTime();
                    creationTime = attrsCreatedTime.toMillis();
                    lastModifiedTime = attrsLastModifiedTime.toMillis();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                FileNodeDto node = new FileNodeDto();
                node.setName(file.getName()); // ファイル名
                long size = file.isDirectory() ? calculateFolderSize(file) : file.length();
                node.setSize(size);
                totalSize += size;
                folderSize += size;
                node.setCreationTime(creationTime); // 作成日時
                node.setLastModified(lastModifiedTime); // 最終更新日時
                node.setFullPath(parentPath + File.separator + file.getName());
                // フォルダーかファイルか
                if (file.isDirectory()) {
                    node.setType("folder");
                    node.setChildren(buildFileTree(file, node.getFullPath()));
                } else {
                    node.setType("file");
                }
                // パーミッション
                node.setPosixFilePermissionDto(makePosixFilePermissionDto(file));
                // 階層構造になっている場合
                fileTree.add(node);
            }
            updateFolderStatistics(parentPath, fileTree.size(), countFiles(fileTree), folderSize,
                    System.nanoTime() - startTime);
        }
        return fileTree;
    }

    private PosixFilePermissionDto makePosixFilePermissionDto(File file) {
        final long startTime = System.nanoTime();
        // PosixFileAttributeView を取得
        PosixFileAttributeView view = Files.getFileAttributeView(file.toPath(), PosixFileAttributeView.class);
        PosixFilePermissionDto posixFilePermissionDto = new PosixFilePermissionDto();
        if (view != null) {
            // PosixFileAttributes を取得
            PosixFileAttributes posixFileAttrs = null;
            try {
                posixFileAttrs = view.readAttributes();
            } catch (IOException e) {
                // TODO 自動生成された catch ブロック
                e.printStackTrace();
            }
            // ファイルのパーミッションを取得
            Set<PosixFilePermission> posixFilePermissions = posixFileAttrs.permissions();

            // ユーザーの権限
            Set<PosixFilePermission> ownerPerms = PosixFilePermissions
                    .fromString(PosixFilePermissions.toString(posixFilePermissions).substring(1, 4));
            // グループの権限
            Set<PosixFilePermission> groupPerms = PosixFilePermissions
                    .fromString(PosixFilePermissions.toString(posixFilePermissions).substring(4, 7));
            // その他のユーザーの権限
            Set<PosixFilePermission> otherPerms = PosixFilePermissions
                    .fromString(PosixFilePermissions.toString(posixFilePermissions).substring(7, 10));

            // 権限の文字列表現
            posixFilePermissionDto.setFilePermissionOfUser(PosixFilePermissions.toString(ownerPerms));
            posixFilePermissionDto.setFilePermissionOfGroup(PosixFilePermissions.toString(groupPerms));
            posixFilePermissionDto.setFilePermissionOfOther(PosixFilePermissions.toString(otherPerms));

        } else {
            // Windows システムであれば、ファイルの権限情報を取得できません
            System.out.println("This platform is not supported PosixFileAttributeView.");
        }
        final long endTime = System.nanoTime();
        System.out.println("[start time][makePosixFilePermissionDto]" + TimeUnit.SECONDS.convert(startTime, TimeUnit.NANOSECONDS) + " sec.");
        System.out.println("[finish time][makePosixFilePermissionDto]" + TimeUnit.SECONDS.convert(endTime, TimeUnit.NANOSECONDS) + " sec.");
        System.out.println("[process time][makePosixFilePermissionDto]" + TimeUnit.SECONDS.convert((endTime - startTime), TimeUnit.NANOSECONDS) + " sec.");
//        System.out.println("[start time][makePosixFilePermissionDto]" + startTime / 1000000000.0 + " sec.");
//        System.out.println("[finish time][makePosixFilePermissionDto]" + endTime / 1000000000.0 + " sec.");
//        System.out.println("[process time][makePosixFilePermissionDto]" + (endTime - startTime) + " sec.");
        return posixFilePermissionDto;

    }

    // フォルダ統計情報を更新
    private void updateFolderStatistics(String folderPath, int folderCount, int fileCount, long totalSize,
            long processTime) {
        FolderStatisticsDto statistics = folderStatisticsMap.getOrDefault(folderPath, new FolderStatisticsDto());
        statistics.setFolderCount(statistics.getFolderCount() + folderCount);
        statistics.setFileCount(statistics.getFileCount() + fileCount);
        statistics.setTotalSize(statistics.getTotalSize() + totalSize);
        statistics.setTotalProcessTime(processTime);
        folderStatisticsMap.put(folderPath, statistics);

        // 合計値更新
        totalFolderCount += folderCount;
        totalFileCount += fileCount;
        totalProcessTime += processTime;
    }

    // フォルダ統計情報を表示
    private void displayFolderStatistics() {
        System.out.println("Folder Statistics:");
        for (Map.Entry<String, FolderStatisticsDto> entry : folderStatisticsMap.entrySet()) {
            String folderPath = entry.getKey();
            FolderStatisticsDto statistics = entry.getValue();
            System.out.println("Folder: " + folderPath);
            System.out.println("  - Folder Count: " + statistics.getFolderCount());
            System.out.println("  - File Count: " + statistics.getFileCount());
            System.out.println("  - Total Size: " + statistics.getTotalSize() + " bytes");
//            System.out.println("  - Total Process Time (seconds): " + statistics.getTotalProcessTime() / 1_000_000_000.0);
            System.out.println("  - Total Process Time (seconds): " + TimeUnit.SECONDS.convert(statistics.getTotalProcessTime(), TimeUnit.NANOSECONDS));

        }
    }

    // 全体の統計情報を表示
    private void displayTotalStatistics() {
        System.out.println("Total Statistics:");
        System.out.println("Total Folder Count: " + totalFolderCount);
        System.out.println("Total File Count: " + totalFileCount);
        System.out.println("Total Size: " + totalSize + " bytes");
//        System.out.println("Total Process Time (seconds): " + totalProcessTime / 1_000_000_000.0);
        System.out.println("  - Total Process Time (seconds): " + TimeUnit.SECONDS.convert(totalProcessTime, TimeUnit.NANOSECONDS));

    }

    // ファイル数をカウントする再帰関数
    private int countFiles(List<FileNodeDto> fileTree) {
        int count = 0;
        for (FileNodeDto node : fileTree) {
            if ("file".equals(node.getType())) {
                count++;
            } else if ("folder".equals(node.getType())) {
                count += countFiles(node.getChildren());
            }
        }
        return count;
    }

    private long calculateFolderSize(File folder) {
        long size = 0;
        File[] files = folder.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    size += calculateFolderSize(file);
                } else {
                    size += file.length();
                }
            }
        }
        return size;
    }
}

フロントエンド側

■C:\Users\Toshinobu\Desktop\soft_work\javascript_work\test-file-manager\.vscode\settings.json

{
    "liveServer.settings.port": 5501
    ,"[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
    },
    "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
    },
    "[typescriptreact]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
    }
    ,  // 自動フォーマット設定
    "editor.formatOnSave": true,   // 保存時にフォーマット
    "editor.formatOnType": true,   // 入力中(改行時)にフォーマット
    "editor.formatOnPaste": true,  // ペースト時にフォーマット
}    

■C:\Users\Toshinobu\Desktop\soft_work\javascript_work\test-file-manager\index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ファイルマネージャー</title>
    <style>
      #container {
        display: flex;
        height: 100vh;
      }
      #folderList {
        min-width: 300px;
        overflow-y: auto;
        border-right: 1px solid #ccc;
      }
      #selectList {
        width: 100%;
      }

      #fileList {
        flex: 2;
        overflow-y: auto;
      }

      .fileListTable {
        text-align: left;
      }

      .folder {
        list-style-type: none;
      }

      .folder > input {
        display: none;
      }

      .folder > label {
        cursor: pointer;
        display: flex; /* チェックボックスとテキストを横並びにする */
        align-items: center; /* 垂直方向に中央揃え */
      }

      .folder > .folderContents {
        display: none;
        margin-left: 20px;
      }

      .folder > input:checked + label + .folderContents {
        display: block;
      }

      .file {
        list-style-type: none;
        margin-left: 20px;
      }

      /* カスタムのコンテキストメニューのスタイル */
      .context-menu {
        background-color: #f9f9f9;
        border: 1px solid #ccc;
        box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
        padding: 4px 0;
        position: absolute;
        z-index: 1000;
      }

      /* コンテキストメニューの項目のスタイル */
      .context-menu div {
        padding: 8px 16px;
        cursor: pointer;
      }

      /* コンテキストメニューの項目のホバー時のスタイル */
      .context-menu div:hover {
        background-color: #ddd;
      }

      /* モーダルのスタイル */
      .modal {
        display: none;
        position: fixed;
        z-index: 1001;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
        background-color: rgba(0, 0, 0, 0.4);
      }

      /* モーダルのコンテンツのスタイル */
      .modal-content {
        background-color: #fefefe;
        margin: 10% auto;
        padding: 20px;
        border: 1px solid #888;
        width: 80%;
        box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2),
          0 6px 20px 0 rgba(0, 0, 0, 0.19);
      }

      /* モーダルのクローズボタンのスタイル */
      .close {
        color: #aaaaaa;
        float: right;
        font-size: 28px;
        font-weight: bold;
      }

      /* モーダルのクローズボタンのホバー時のスタイル */
      .close:hover,
      .close:focus {
        color: #000;
        text-decoration: none;
        cursor: pointer;
      }

      /* モーダル内のリストのスタイル */
      .property-list {
        list-style-type: none;
        padding: 0;
      }

      .property-list li {
        margin-bottom: 10px;
      }

      .property-list li span {
        font-weight: bold;
      }
      #folderTree .file {
        margin-left: 0; /* 最上位のファイルにマージンを適用しない */
      }
      .permissions-container {
        margin-left: 1rem;
      }
      .permission-matrix {
        display: flex;
      }
      .permission-label {
        min-width: 72px;
      }
      #folderPathValue {
        display: block;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div id="container">
      <div id="folderList">
        <ul id="folderTree"></ul>
      </div>
      <div id="selectList">
        <div id="folderPath">
          <input type="text" id="folderPathValue" value="" />
        </div>
        <div id="fileList"></div>
      </div>
    </div>
    <!-- プロパティモーダル -->
    <div id="propertyModal" class="modal">
      <div class="modal-content">
        <span class="close">×</span>
        <ul class="property-list" id="propertyContent">
          <!-- プロパティの内容がここに表示されます。 -->
        </ul>
      </div>
    </div>

    <script>
      function getFolderFullPath(tree, folderName) {
        function getCommonPath(path1, path2) {
          const splitPath1 = path1.split(/[\\/]/); // バックスラッシュまたはスラッシュで分割
          const splitPath2 = path2.split(/[\\/]/); // バックスラッシュまたはスラッシュで分割
          let commonPath = "";
          for (
            let i = 0;
            i < Math.min(splitPath1.length, splitPath2.length);
            i++
          ) {
            if (splitPath1[i] === splitPath2[i]) {
              commonPath += splitPath1[i] + "/";
            } else {
              break;
            }
          }
          return commonPath;
        }

        function getRelativePath(fullPath, commonPath) {
          return fullPath.substring(commonPath.length);
        }

        let fullPath = "";
        for (const item of tree) {
          if (item.name === folderName) {
            return item.fullPath; // マッチするフォルダが見つかったらそのフルパスを返す
          }
          if (item.children) {
            const childPath = getFolderFullPath(item.children, folderName);
            if (childPath) {
              const commonPath = getCommonPath(item.fullPath, childPath);
              const relativePath = getRelativePath(childPath, commonPath);
              const pathSeparator = item.fullPath.includes("\\") ? "\\" : "/";
              return item.fullPath + pathSeparator + relativePath;
            }
          }
        }
        return fullPath; // マッチするフォルダが見つからない場合は空文字列を返す
      }

      // フォルダのフルパスを表示する関数
      function updateFolderPath(folderPath) {
        const folderPathElement = document
          .getElementById("folderPath")
          .getElementsByTagName("input")[0];
        if (folderPathElement) {
          folderPathElement.value = folderPath;
        }
      }

      let fileTreeData; // ファイルツリーデータを格納する変数
      function getFileTree() {
        fetch("http://localhost:8080/filetree")
          .then((response) => {
            if (!response.ok) {
              throw new Error("Network response was not ok");
            }
            const contentType = response.headers.get("content-type");
            if (
              contentType &&
              contentType.includes("application/octet-stream")
            ) {
              return response.blob(); // バイナリ形式のデータを取得
            } else {
              return response.json(); // JSON形式のファイルツリーデータを取得
            }
          })
          .then((data) => {
            if (typeof data === "string") {
              displayTextFileContents(data); // テキスト形式のデータを表示
            } else {
              fileTreeData = data;
              displayFileTree(data, document.getElementById("folderTree"));
            }
          })
          .then(() => {
            // 左サイドバーのフォルダがクリックされた時のイベントを登録
            const folderList = document.querySelectorAll("li.folder");
            for (var idx = 0; idx < folderList.length; idx++) {
              if (folderList[idx]) {
                folderList[idx].addEventListener("click", (event) => {
                  console.dir(event);
                  if (event.target && event.target.nodeName === "LABEL") {
                    // const folderName = event.target.textContent;
                    const folderFullPath =
                      event.target.parentElement.dataset.fullPath;
                    // const folderFullPath = getFolderFullPath(
                    //   fileTreeData,
                    //   folderName
                    // ); // フォルダのフルパスを取得

                    renderFileList(folderFullPath); // フォルダのフルパスを渡す
                    updateFolderPath(folderFullPath); // 右上にフォルダのフルパスを表示
                  }
                });
              }
            }
          })
          .catch((error) => console.error("Error fetching file tree:", error));
      }

      // ファイルツリーを表示
      function displayFileTree(fileTreeData, parentElement) {
        fileTreeData.forEach((node) => {
          if (node.type === "folder") {
            const li = document.createElement("li");
            li.className = "folder";
            const label = document.createElement("label");
            const input = document.createElement("input");
            input.type = "checkbox";

            // 右クリックでコンテキストメニューを表示
            label.addEventListener("contextmenu", function (event) {
              event.preventDefault(); // デフォルトのコンテキストメニューをキャンセル
              showContextMenu(event.clientX, event.clientY, node); // カスタムのコンテキストメニューを表示
            });

            label.appendChild(input); // inputをlabelの子要素として追加
            label.appendChild(document.createTextNode(node.name)); // テキストを追加
            li.appendChild(label);

            // data 属性に fullPath の値を設定
            li.dataset.fullPath = node.fullPath;

            if (node.children) {
              const folderContents = document.createElement("ul");
              folderContents.className = "folderContents"; // クラス名を追加
              folderContents.style.display = "none"; // 追加:最初は非表示
              displayFileTree(node.children, folderContents);
              li.appendChild(folderContents);

              // すべての子要素に fullPath を設定する
              // node.children.forEach((childNode) => {
              //   const childLi = document.createElement("li");
              //   childLi.textContent = childNode.name;
              //   childLi.dataset.fullPath = childNode.fullPath;
              //   //folderContents.appendChild(childLi);
              // });

              // チェックボックスの状態変化に応じて子要素を表示または非表示にする
              input.addEventListener("change", function () {
                folderContents.style.display = input.checked ? "block" : "none";
              });
            }

            parentElement.appendChild(li);
          }
        });
      }

      // テキストファイルの内容を表示
      function displayTextFileContents(fileContent) {
        const textarea = document.createElement("textarea");
        textarea.textContent = fileContent; // ファイルの内容をテキストエリアに表示
        textarea.style.width = "100%";
        textarea.style.height = "300px"; // 適切なサイズに調整してください
        document.body.appendChild(textarea);
      }

      // カスタムのコンテキストメニューを表示する関数
      function showContextMenu(x, y, node) {
        // 既存のコンテキストメニューを削除
        const existingContextMenu = document.querySelector(".context-menu");
        if (existingContextMenu) {
          existingContextMenu.remove();
        }

        // メニューを表示するためのHTML要素を生成
        const contextMenu = document.createElement("div");
        contextMenu.className = "context-menu";
        contextMenu.style.left = x + "px";
        contextMenu.style.top = y + "px";

        // 新規作成、編集、リネーム、削除メニュー項目を追加
        if (node.type === "folder") {
          addItem("新規作成", function () {
            console.log("新規作成");
          });
          addItem("リネーム", function () {
            console.log("リネーム");
          });
        } else {
          addItem("編集", function () {
            console.log("編集");
          });
        }
        addItem("削除", function () {
          console.log("削除");
        });
        addItem("プロパティ", function () {
          showPropertyModal(node);
        });

        // ドキュメントにメニューを追加
        document.body.appendChild(contextMenu);

        // コンテキストメニューを非表示にするためのイベントリスナーを追加
        document.addEventListener(
          "click",
          function () {
            contextMenu.remove(); // メニューを削除
          },
          { once: true }
        ); // 一度だけ実行されるように設定

        // メニュー項目を追加する関数
        function addItem(label, action) {
          const item = document.createElement("div");
          item.textContent = label;
          item.addEventListener("click", action);
          contextMenu.appendChild(item);
        }
      }

      // パーミッションをレンダリングする関数
      function renderPermissions(posixFilePermissionDto) {
        console.table(posixFilePermissionDto);

        if (!posixFilePermissionDto) return ""; // パーミッション情報がない場合は空文字列を返す

        const permissionsArray = [
          posixFilePermissionDto.filePermissionOfUser,
          posixFilePermissionDto.filePermissionOfGroup,
          posixFilePermissionDto.filePermissionOfOther,
        ];
        const permissionLabels = ["読み込み", "書き込み", "実行"];
        const permissionGroups = ["ユーザー", "グループ", "その他"];

        let permissionsHTML = "";

        for (let i = 0; i < 3; i++) {
          permissionsHTML += `<div class="permission-matrix"><div class="permission-label"><strong>${permissionGroups[i]}</strong></div><div>: `;
          for (let j = 0; j < 3; j++) {
            const index = i * 3 + j;
            const checked =
              permissionsArray[i] && permissionsArray[i][j] === "1"
                ? "checked"
                : "";
            permissionsHTML += `<label><input type="checkbox" ${checked} disabled> ${permissionLabels[j]}</label>`;
          }
          permissionsHTML += "</div></div>";
        }
        console.log("permissionsHTML");
        console.dir(permissionsHTML);

        return permissionsHTML;
      }

      // プロパティの詳細情報を表示する関数
      function showPropertyModal(node) {
        // プロパティモーダルを表示する処理を実装する
        const propertyModal = document.getElementById("propertyModal");
        const propertyContent = document.getElementById("propertyContent");
        propertyContent.innerHTML = ""; // 既存の内容をクリア

        const propertyItems = [
          { label: "名前", value: node.name },
          { label: "フルパス", value: node.fullPath },
          { label: "サイズ", value: node.size },
          { label: "所有ユーザー", value: node.owner },
          { label: "所有グループ", value: node.group },
          {
            label: "パーミッション",
            value: renderPermissions(node.posixFilePermissionDto),
          },
          { label: "作成日", value: formatDate(new Date(node.creationTime)) },
          { label: "更新日", value: formatDate(new Date(node.lastModified)) },
        ];

        propertyItems.forEach((item) => {
          const li = document.createElement("li");
          const label = document.createElement("span");
          label.textContent = item.label + ": ";
          const value = document.createTextNode(item.value);
          const valueContainer = document.createElement("span");
          valueContainer.appendChild(value);
          li.appendChild(label);

          // パーミッションの場合は特別な処理を行う
          if (item.label === "パーミッション") {
            const permissionsContainer = document.createElement("div");
            permissionsContainer.className = "permissions-container"; // クラス属性を追加
            permissionsContainer.innerHTML = item.value; // ここを修正する
            li.appendChild(permissionsContainer);
          } else {
            li.appendChild(valueContainer);
          }

          propertyContent.appendChild(li);
        });

        propertyModal.style.display = "block";

        // モーダルのクローズボタンの処理を追加
        const closeBtn = document.getElementsByClassName("close")[0];
        closeBtn.onclick = function () {
          propertyModal.style.display = "none";
        };
      }

      // 日付を指定された形式にフォーマットする関数
      function formatDate(date) {
        const year = date.getFullYear();
        const month = ("0" + (date.getMonth() + 1)).slice(-2);
        const day = ("0" + date.getDate()).slice(-2);
        const hours = ("0" + date.getHours()).slice(-2);
        const minutes = ("0" + date.getMinutes()).slice(-2);
        const seconds = ("0" + date.getSeconds()).slice(-2);
        return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
      }

      // フルパスからNodeを取得する関数
      function getNodeByPath(tree, path) {
        // フォルダのフルパスをデリミタで分割して配列にする
        const pathElements = path
          .split(/[\\/]/)
          .filter((element) => element !== ""); // 空の要素を除外

        function findNodeInTree(nodes, pathElements) {
          for (const node of nodes) {
            const nodePathElements = node.fullPath
              .split(/[\\/]/)
              .filter((element) => element !== "");
            // フォルダ名が全て一致するか確認
            if (
              nodePathElements.length === pathElements.length &&
              nodePathElements.every(
                (element, index) => element === pathElements[index]
              )
            ) {
              return node;
            }
            // itemにchildrenがあり、その中にフォルダ名が一致するものがあるか確認
            if (node.children) {
              const foundInChildren = findNodeInTree(
                node.children,
                pathElements
              );
              if (foundInChildren) {
                return foundInChildren;
              }
            }
          }
          return null;
        }

        return findNodeInTree(tree, pathElements);
      }

      // 右側のフォルダとファイルの一覧を表示する関数
      function renderFileList(folderPath) {
        // 指定されたフォルダの情報を取得
        console.log(folderPath);
        const node = getNodeByPath(fileTreeData, folderPath);
        console.dir(node);
        if (node) {
          // フォルダ内のファイルとフォルダのリストを取得
          const folderContents = node.children;

          const fileListContainer = document.getElementById("fileList");
          const fullPathContainer = document.getElementById("folderPath");
          fileListContainer.innerHTML = ""; // 一覧をクリア
          // 右側の上部にフルパスを表示
          fullPathContainer.textContent = folderPath;

          const table = document.createElement("table"); // テーブル要素を作成
          table.classList.add("fileListTable"); // 追加:スタイリングのためのクラスを追加

          // テーブルヘッダーを作成
          const headerRow = document.createElement("tr");
          const headers = [
            "名前",
            "所有ユーザー",
            "所有グループ",
            "Type",
            "作成日",
            "更新日",
          ];
          headers.forEach((headerText) => {
            const headerCell = document.createElement("th");
            headerCell.textContent = headerText;
            headerRow.appendChild(headerCell);
          });
          table.appendChild(headerRow);

          folderContents.forEach((item) => {
            const row = document.createElement("tr"); // 新しい行を作成

            const nameCell = document.createElement("td");
            nameCell.textContent = item.name;
            row.appendChild(nameCell);
            const ownerCell = document.createElement("td");
            ownerCell.textContent = item.owner;
            row.appendChild(ownerCell);
            const groupCell = document.createElement("td");
            groupCell.textContent = item.group;
            row.appendChild(groupCell);
            const typeCell = document.createElement("td");
            typeCell.textContent = item.type;
            row.appendChild(typeCell);
            const creationTimeCell = document.createElement("td");
            creationTimeCell.textContent = formatDate(
              new Date(item.creationTime)
            );
            row.appendChild(creationTimeCell);
            const lastModifiedCell = document.createElement("td");
            lastModifiedCell.textContent = formatDate(
              new Date(item.lastModified)
            );
            row.appendChild(lastModifiedCell);

            row.dataset.fullPath = item.fullPath; // item.fullPathを行に設定
            row.addEventListener("contextmenu", (event) => {
              event.preventDefault(); // デフォルトのコンテキストメニューをキャンセル
              showContextMenu(event.clientX, event.clientY, item); // カスタムのコンテキストメニューを表示
            });

            // フォルダの場合はクリックイベントを追加してその中身を表示
            if (item.type === "folder") {
              row.addEventListener("click", (event) => {
                const folderFullPath =
                  event.target.parentElement.dataset.fullPath;
                // const folderFullPath = getFolderFullPath(
                //   fileTreeData,
                //   item.fullPath
                // );
                console.log(folderFullPath);
                renderFileList(folderFullPath); // フォルダのフルパスを渡すように修正
              });
            }

            table.appendChild(row); // テーブルに行を追加
          });
          fileListContainer.appendChild(table); // テーブルを親要素に追加
        } else {
          console.log("Folder not found or is not a folder.");
        }
      }

      // 初期表示
      getFileTree();
    </script>
  </body>
</html>
    

で、サーバサイド側のアプリケーションは、EclipseでSpringBootプロジェクトを起動。

フロントエンド側のアプリケーションは、Visual Studio Code拡張機能の「Live Server」ってので起動してます。

フロントエンド側

サーバサイド側

う~む、そもそも、Windowsエクスプローラーの仕組みが分からんので、真似しようがないんだけど...

仕様が分からないから雰囲気で作ったけど、計測の実装が合っているとすると、フォルダ数、ファイル数を合わせてたかだか160000弱の数を処理するのに、177秒の時間を要しているのは厳しいですな...

う~む、計測が上手くいっていないっぽい、ChatGPTに任せたのマズかったようですな。

実際には、

⇧ 28000弱の数なんだけど、52秒ぐらいかかってた。

ChatGPT、ちょっと酷過ぎる...

結局、自力で修正することになったのだけど、合計処理時間は直しきれなかった...

■/test-web-file-system/src/main/java/com/example/demo/TestWebFileSystemApplication.java

package com.example.demo.controller;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.dto.FileNodeDto;
import com.example.demo.dto.FolderStatisticsDto;
import com.example.demo.dto.PosixFilePermissionDto;

@RestController
public class FileTreeController {
    private final Map<String, FolderStatisticsDto> folderStatisticsMap = new HashMap<>();
    private static int totalFolderCount = 0;
    private static int totalFileCount = 0;
    private static long totalSize = 0;
    private static long totalProcessTime = 0;

    @GetMapping("/filetree")
    public List<FileNodeDto> getFileTree() {
        String rootPath = "C:\\Users\\Toshinobu\\Desktop\\soft_work\\javascript_work";
        File rootDirectory = new File(rootPath);
        long startTime = TimeUnit.SECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS);     
        List<FileNodeDto> fileNodeDto = buildFileTree(rootDirectory, rootDirectory.getAbsolutePath());
        displayFolderStatistics();
        displayTotalStatistics();
        long finishTime = TimeUnit.SECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS);
        System.out.println(startTime);
        System.out.println(finishTime);
        System.out.println((finishTime - startTime));
        return fileNodeDto;
    }

    private List<FileNodeDto> buildFileTree(File directory, String parentPath) {
        final long startTime = System.nanoTime();
        List<FileNodeDto> fileTree = new ArrayList<>();
        File[] files = directory.listFiles();
        BasicFileAttributes basicFileAttrs;
        long creationTime = 0;
        long lastModifiedTime = 0;
        long totalSize = 0;
        long folderSize = 0;
        int folderCount = 0;
        int fileCount = 0;

        if (files != null) {

            for (File file : files) {
                try {
                    basicFileAttrs = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
                    FileTime attrsCreatedTime = basicFileAttrs.creationTime();
                    FileTime attrsLastModifiedTime = basicFileAttrs.lastModifiedTime();
                    creationTime = attrsCreatedTime.toMillis();
                    lastModifiedTime = attrsLastModifiedTime.toMillis();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                FileNodeDto node = new FileNodeDto();
                node.setName(file.getName()); // ファイル名
                long size = file.isDirectory() ? calculateFolderSize(file) : file.length();
                node.setSize(size);
                // folderSize += size;
                totalSize += basicFileAttrs.size();
                node.setCreationTime(creationTime); // 作成日時
                node.setLastModified(lastModifiedTime); // 最終更新日時
                node.setFullPath(parentPath + File.separator + file.getName());
                // フォルダーかファイルか
                if (file.isDirectory()) {
                    node.setType("folder");
                    folderCount++;
                    node.setChildren(buildFileTree(file, node.getFullPath()));
                } else {
                    node.setType("file");
                    fileCount++;
                }
                // パーミッション
                node.setPosixFilePermissionDto(makePosixFilePermissionDto(file));
                // 階層構造になっている場合
                fileTree.add(node);
            }
            long processTime =System.nanoTime() - startTime;
            updateFolderStatistics(parentPath, folderCount, fileCount, folderSize, processTime);
        }
        return fileTree;
    }

    private PosixFilePermissionDto makePosixFilePermissionDto(File file) {
        final long startTime = System.nanoTime();
        // PosixFileAttributeView を取得
        PosixFileAttributeView view = Files.getFileAttributeView(file.toPath(), PosixFileAttributeView.class);
        PosixFilePermissionDto posixFilePermissionDto = new PosixFilePermissionDto();
        if (view != null) {
            // PosixFileAttributes を取得
            PosixFileAttributes posixFileAttrs = null;
            try {
                posixFileAttrs = view.readAttributes();
            } catch (IOException e) {
                // TODO 自動生成された catch ブロック
                e.printStackTrace();
            }
            // ファイルのパーミッションを取得
            Set<PosixFilePermission> posixFilePermissions = posixFileAttrs.permissions();

            // ユーザーの権限
            Set<PosixFilePermission> ownerPerms = PosixFilePermissions
                    .fromString(PosixFilePermissions.toString(posixFilePermissions).substring(1, 4));
            // グループの権限
            Set<PosixFilePermission> groupPerms = PosixFilePermissions
                    .fromString(PosixFilePermissions.toString(posixFilePermissions).substring(4, 7));
            // その他のユーザーの権限
            Set<PosixFilePermission> otherPerms = PosixFilePermissions
                    .fromString(PosixFilePermissions.toString(posixFilePermissions).substring(7, 10));

            // 権限の文字列表現
            posixFilePermissionDto.setFilePermissionOfUser(PosixFilePermissions.toString(ownerPerms));
            posixFilePermissionDto.setFilePermissionOfGroup(PosixFilePermissions.toString(groupPerms));
            posixFilePermissionDto.setFilePermissionOfOther(PosixFilePermissions.toString(otherPerms));

        } else {
            // Windows システムであれば、ファイルの権限情報を取得できません
            System.out.println("This platform is not supported PosixFileAttributeView.");
        }
        final long endTime = System.nanoTime();
        
        System.out.println("[start time][makePosixFilePermissionDto]" + TimeUnit.SECONDS.convert(startTime, TimeUnit.NANOSECONDS) + " sec.");
        System.out.println("[finish time][makePosixFilePermissionDto]" + TimeUnit.SECONDS.convert(endTime, TimeUnit.NANOSECONDS) + " sec.");
        System.out.println("[process time][makePosixFilePermissionDto]" + TimeUnit.SECONDS.convert((endTime - startTime), TimeUnit.NANOSECONDS) + " sec.");
//        System.out.println("[start time][makePosixFilePermissionDto]" + startTime / 1000000000.0 + " sec.");
//        System.out.println("[finish time][makePosixFilePermissionDto]" + endTime / 1000000000.0 + " sec.");
//        System.out.println("[process time][makePosixFilePermissionDto]" + (endTime - startTime) + " sec.");
        return posixFilePermissionDto;

    }

    // フォルダ統計情報を更新
    private void updateFolderStatistics(String folderPath, int folderCount, int fileCount, long totalSize,
            long processTime) {
        FolderStatisticsDto statistics = folderStatisticsMap.getOrDefault(folderPath, new FolderStatisticsDto());
        statistics.setFolderCount(statistics.getFolderCount() + folderCount);
        statistics.setFileCount(statistics.getFileCount() + fileCount);
        statistics.setTotalSize(statistics.getTotalSize() + totalSize);
        statistics.setTotalProcessTime(processTime);
        folderStatisticsMap.put(folderPath, statistics);

        // 合計値更新
        totalFolderCount += folderCount;
        totalFileCount += fileCount;
        totalSize += folderSize; // 合計サイズにフォルダのサイズを加算
        totalProcessTime += processTime;
    }

    // フォルダ統計情報を表示
    private void displayFolderStatistics() {
        System.out.println("Folder Statistics:");
        for (Map.Entry<String, FolderStatisticsDto> entry : folderStatisticsMap.entrySet()) {
            String folderPath = entry.getKey();
            FolderStatisticsDto statistics = entry.getValue();
            System.out.println("Folder: " + folderPath);
            System.out.println("  - Folder Count: " + statistics.getFolderCount());
            System.out.println("  - File Count: " + statistics.getFileCount());
            System.out.println("  - Total Size: " + statistics.getTotalSize() + " bytes");
//            System.out.println("  - Total Process Time (seconds): " + statistics.getTotalProcessTime() / 1_000_000_000.0);
            System.out.println("  - Total Process Time (seconds): " + TimeUnit.SECONDS.convert(statistics.getTotalProcessTime(), TimeUnit.NANOSECONDS));
        }
    }

    // 全体の統計情報を表示
    private void displayTotalStatistics() {
        System.out.println("Total Statistics:");
        System.out.println("Total Folder Count: " + totalFolderCount);
        System.out.println("Total File Count: " + totalFileCount);
        System.out.println("Total Size: " + totalSize + " bytes");
//        System.out.println("Total Process Time (seconds): " + totalProcessTime / 1_000_000_000.0);
        System.out.println("  - Total Process Time (seconds): " + TimeUnit.SECONDS.convert(totalProcessTime, TimeUnit.NANOSECONDS));
    }

    // ファイル数をカウントする再帰関数
    private int countFiles(List<FileNodeDto> fileTree) {
        int count = 0;
        for (FileNodeDto node : fileTree) {
            if ("file".equals(node.getType())) {
                count++;
            } else if ("folder".equals(node.getType())) {
                count += countFiles(node.getChildren());
            }
        }
        return count;
    }

    private long calculateFolderSize(File folder) {
        long size = 0;
        File[] files = folder.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    size += calculateFolderSize(file);
                } else {
                    size += file.length();
                }
            }
        }
        return size;
    }
}

時間が取れたら、修正を頑張ってみますかね...

2024年2月19日(月)追記:↓ ここから

なるほど、再帰処理で、フォルダーの入れ子の数が異なると、処理時間で重複してくる部分があるから、合計処理時間については、最上位のフォルダーが処理された時間ということになるようなので、単純に再帰処理毎の処理時間を合算する方法で求めるのは不可能だったという話...

やってくれましたな、ChatGPTさん...

⇧ 47秒で一致しているので気付けたと。

泥臭いやり方だけど、処理の仕組みが理解できなかったり、スマートな方法が思いつかない時は、愚直に突き進まざるを得ない時もあると。

まぁ、アルゴリズムに精通している有識者に聞けば瞬殺だとは思われますが...

話を元に戻すと、なので、全ての処理が完了するまでの処理時間は、再帰処理の外側で求めるのが良いということですな。

最終的なJavaソースコードは、以下で。他のソースコードは、変更が無いので、前回のものを利用でOK。

■/test-web-file-system/src/main/java/com/example/demo/dto/FolderStatisticsDto.java

package com.example.demo.dto;

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class FolderStatisticsDto {
    private int folderCount = 0;
    private int fileCount = 0;
    private long totalSize = 0;
    private long totalProcessTime = 0;
    private LocalDateTime startDateTime;
    private LocalDateTime finishDateTime;
}    

■/test-web-file-system/src/main/java/com/example/demo/controller/FileTreeController.java

package com.example.demo.controller;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.dto.FileNodeDto;
import com.example.demo.dto.FolderStatisticsDto;
import com.example.demo.dto.PosixFilePermissionDto;

@RestController
public class FileTreeController {
	private final Map<String, FolderStatisticsDto> folderStatisticsMap = new HashMap<>();
	private static int totalFolderCount = 0;
	private static int totalFileCount = 0;
	private static long totalSize = 0;
	private static long totalProcessTime = 0;

	@GetMapping("/filetree")
	public List<FileNodeDto> getFileTree() {

		// 対象のディレクトリ
		String rootPath = "C:\\Users\\Toshinobu\\Desktop\\soft_work\\javascript_work";
		File rootDirectory = new File(rootPath);

		// 処理開始時間
		LocalDateTime startDateTime = LocalDateTime.now();
		
		// フロントエンド側に返す情報を作成する
		List<FileNodeDto> fileNodeDto = buildFileTree(rootDirectory, rootDirectory.getAbsolutePath());
		
		// 処理終了時間
		LocalDateTime finishDateTime = LocalDateTime.now();
		
		// 処理時間の算出
		Instant instantStartTime = startDateTime.atZone(ZoneId.systemDefault()).toInstant();
		Instant instantFinishTime = finishDateTime.atZone(ZoneId.systemDefault()).toInstant();
		long elapsedTime = TimeUnit.NANOSECONDS.convert(instantFinishTime.getEpochSecond(), TimeUnit.SECONDS)
				- TimeUnit.NANOSECONDS.convert(instantStartTime.getEpochSecond(), TimeUnit.SECONDS);

		// 統計
		displayFolderStatistics();
		displayTotalStatistics();

		// 処理開始日時を出力
		System.out.println("Start time" + startDateTime);
		// 処理終了日時を出力
		System.out.println("Finish time" + finishDateTime);
		// 処理時間(秒)を出力
		System.out.println("Total time: " + TimeUnit.SECONDS.convert(elapsedTime, TimeUnit.NANOSECONDS) + " seconds");

		// フロントエンド側にレスポンスを返す
		return fileNodeDto;
	}

	private List<FileNodeDto> buildFileTree(File directory, String parentPath) {
		LocalDateTime startDateTime = LocalDateTime.now(); // 処理開始日時
		LocalDateTime finishDateTime = null; // 処理終了日時
		long processTime = 0; // 処理時間

		List<FileNodeDto> fileTree = new ArrayList<>();
		// ディレクトリに含まれるファイル、または、フォルダの一覧
		File[] files = directory.listFiles();
		BasicFileAttributes basicFileAttrs;

		int folderCount = 0; // フォルダー数カウント用
		int fileCount = 0; // ファイル数カウント用
		long totalSize = 0; // 合計サイズ

		if (files != null) {
			for (File file : files) {
				try {
					// ファイルの作成日、更新日の情報を取得
					basicFileAttrs = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
					FileTime attrsCreatedTime = basicFileAttrs.creationTime();
					FileTime attrsLastModifiedTime = basicFileAttrs.lastModifiedTime();
					long creationTime = attrsCreatedTime.toMillis();
					long lastModifiedTime = attrsLastModifiedTime.toMillis();

					// フロントエンド側に返す情報を作成する
					FileNodeDto node = new FileNodeDto();
					// ファイル名、または、フォルダ名を設定
					node.setName(file.getName());
					// サイズを設定
					long size = file.isDirectory() ? calculateFolderSize(file) : file.length();
					node.setSize(size);
					//                    folderSize += size;
					// 総合計のサイズ
					totalSize += basicFileAttrs.size();
					// 作成日を設定
					node.setCreationTime(creationTime);
					// 更新日を設定
					node.setLastModified(lastModifiedTime);
					// フルパスを設定
					node.setFullPath(parentPath + File.separator + file.getName());
					
					// フォルダの場合
					if (file.isDirectory()) {
						node.setType("folder");
						folderCount++;
						node.setChildren(buildFileTree(file, node.getFullPath()));
				    // ファイルの場合
					} else {
						node.setType("file");
						fileCount++;
					}
					// 処理時間を算出
					finishDateTime = LocalDateTime.now();
					Instant instantStartTime = startDateTime.atZone(ZoneId.systemDefault()).toInstant();
					Instant instantFinishTime = finishDateTime.atZone(ZoneId.systemDefault()).toInstant();
					processTime = TimeUnit.NANOSECONDS.convert(instantFinishTime.getEpochSecond(), TimeUnit.SECONDS)
							- TimeUnit.NANOSECONDS.convert(instantStartTime.getEpochSecond(), TimeUnit.SECONDS);

					// ファイル権限を設定
					node.setPosixFilePermissionDto(makePosixFilePermissionDto(file));
					// リストに追加
					fileTree.add(node);

				} catch (IOException e) {
					e.printStackTrace();

				}
			}
		}
		
		// 統計を更新
		updateFolderStatistics(parentPath, folderCount, fileCount, totalSize, processTime, startDateTime,
				finishDateTime);
		return fileTree;
	}

	private PosixFilePermissionDto makePosixFilePermissionDto(File file) {

		// PosixFileAttributeView を取得
		PosixFileAttributeView view = Files.getFileAttributeView(file.toPath(), PosixFileAttributeView.class);
		PosixFilePermissionDto posixFilePermissionDto = new PosixFilePermissionDto();
		if (view != null) {
			// PosixFileAttributes を取得
			try {
				PosixFileAttributes posixFileAttrs = view.readAttributes();
				// ファイルのパーミッションを取得
				Set<PosixFilePermission> posixFilePermissions = posixFileAttrs.permissions();

				// ユーザーの権限
				Set<PosixFilePermission> ownerPerms = PosixFilePermissions
						.fromString(PosixFilePermissions.toString(posixFilePermissions).substring(1, 4));
				// グループの権限
				Set<PosixFilePermission> groupPerms = PosixFilePermissions
						.fromString(PosixFilePermissions.toString(posixFilePermissions).substring(4, 7));
				// その他のユーザーの権限
				Set<PosixFilePermission> otherPerms = PosixFilePermissions
						.fromString(PosixFilePermissions.toString(posixFilePermissions).substring(7, 10));

				// 権限の文字列表現
				posixFilePermissionDto.setFilePermissionOfUser(PosixFilePermissions.toString(ownerPerms));
				posixFilePermissionDto.setFilePermissionOfGroup(PosixFilePermissions.toString(groupPerms));
				posixFilePermissionDto.setFilePermissionOfOther(PosixFilePermissions.toString(otherPerms));

			} catch (IOException e) {
				e.printStackTrace();

			}
		} else {
			// Windows システムであれば、ファイルの権限情報を取得できません
			System.out.println("This platform is not supported PosixFileAttributeView.");

		}
		return posixFilePermissionDto;
	}

	// フォルダ統計情報を更新
	private void updateFolderStatistics(String folderPath, int folderCount, int fileCount, long folderSize,
			long processTime, LocalDateTime startDateTime, LocalDateTime finsihDateTime) {

		FolderStatisticsDto statistics = folderStatisticsMap.getOrDefault(folderPath, new FolderStatisticsDto());
		statistics.setFolderCount(statistics.getFolderCount() + folderCount);
		statistics.setFileCount(statistics.getFileCount() + fileCount);
		statistics.setTotalSize(statistics.getTotalSize() + folderSize); // 合計サイズにフォルダのサイズを加算
		statistics.setTotalProcessTime(statistics.getTotalProcessTime() + processTime);
		statistics.setStartDateTime(startDateTime);
		statistics.setFinishDateTime(finsihDateTime);
		folderStatisticsMap.put(folderPath, statistics);

		totalFolderCount += folderCount;
		totalFileCount += fileCount;
		totalSize += folderSize; // 合計サイズにフォルダのサイズを加算
		totalProcessTime += processTime;
	}

	// フォルダ統計情報を表示
	private void displayFolderStatistics() {
		System.out.println("Folder Statistics:");
		for (Map.Entry<String, FolderStatisticsDto> entry : folderStatisticsMap.entrySet()) {
			String folderPath = entry.getKey();
			FolderStatisticsDto statistics = entry.getValue();
			System.out.println("Folder: " + folderPath);
			System.out.println("  - Folder Count: " + statistics.getFolderCount());
			System.out.println("  - File Count: " + statistics.getFileCount());
			System.out.println("  - Total Size: " + statistics.getTotalSize() + " bytes");
			System.out.println("  - StarteDateTime: " + statistics.getStartDateTime());
			System.out.println("  - FinishDateTime: " + statistics.getFinishDateTime());
			System.out.println("  - Total Process Time (seconds): "
					+ TimeUnit.SECONDS.convert(statistics.getTotalProcessTime(), TimeUnit.NANOSECONDS));
		}
	}

	private void displayTotalStatistics() {
		System.out.println("Total Statistics:");
		System.out.println("Total Folder Count: " + totalFolderCount);
		System.out.println("Total File Count: " + totalFileCount);
		System.out.println("Total Size: " + totalSize + " bytes");
		System.out.println("Total Process Time (seconds): "
				+ TimeUnit.SECONDS.convert(totalProcessTime, TimeUnit.NANOSECONDS));
	}

	private long calculateFolderSize(File folder) {
		long size = 0;
		File[] files = folder.listFiles();
		if (files != null) {
			for (File file : files) {
				if (file.isDirectory()) {
					size += calculateFolderSize(file);
				} else {
					size += file.length();
				}
			}
		}
		return size;
	}

}
    

⇧ コントローラーの処理は、フロントエンド側からリクエスト受け取るメソッド以外は、サービスクラスとかに外出しした方が良いと思うんだけど、今回は割愛。

2024年2月19日(月)追記:↑ ここまで

処理時間短縮に、ページングとかを使う方が良いんだろうか?

それだと、フロントエンドとサーバサイドの通信の頻度が多くはなると思うんだけど、正解が分からん...

毎度モヤモヤ感が半端ない...

今回はこのへんで。