← All writing
Development

node.js section8 file system note taking

좀 뭔가 헷갈리는 게 많음

file?

  • 의미있는 0과 1의 시퀀스
  • 디코딩 방식에 따라 완전히 달라짐
  • 메타 데이터: 데이터를 위한 설명서
  • unix executable 은 os에서 직접 맞물려 돌아가는 native program
  • node.js는 직접 하드웨어를 제어하지 않는다.
  • node.js는 운영체제 위에서 동작하는 하나의 프로세스일 뿐

system call

  • nodejs와 운영체제가 소통하는 표준 창구를 system call이라 부른다.
  • nodejs는 system call을 운영체제에 전달하기 위해 내부적으로 c언어 기반의 비동기 i/o 라이브러리인 libuv를 사용한다.
  • js code > libuv > os system call
  • libuv내에는 파일 관련 작업을 처리하는 thread pool이 존재한다.

promise api

  • 동기 api의 치명적인 한계 main thread blocking
  • (error) => {} error first callback 에러 우선 콜백

file command center example

  • 상태기반 중복제거 state-based deduplication
  • 상태 state를 기준으로 중복 이벤트를 걸러내는 알고리즘
  • timer based debounce
  • greedy vs lazy in regular expression
import {
  access,
  writeFile,
  watch,
  open,
  stat,
  unlink,
  rename,
  appendFile,
} from "node:fs/promises";
// promise에서 import할 것
import { Buffer } from "node:buffer";

const FILE_COMMANDS = "./file-commands.txt";

// 파일이 존재하고, 접근 가능한지 검증
const fileExist = async (path) => {
  try {
    await access(path);
    return true;
  } catch {
    return false;
  }
};

// 실제 커맨드들을 실행하는 함수
const executeCommand = async (commandString) => {
  const command = commandString.trim();
  if (!command) return;

  if (command.startsWith("create a file ")) {
    const filePath = command.replace("create a file ", "").trim();
    await createFile(filePath);
  } 

  else if (command.startsWith("delete a file ")) {
    const filePath = command.replace("delete a file ", "").trim();
    await deleteFile(filePath);
  } 
  
  else if (command.startsWith("rename the file ")) {
    const match = command.match(/^rename the file (.+) to (.+)$/);
    if (match) {
      await renameFile(match[1].trim(), match[2].trim());
    } else {
      console.log(`⚠️ there is no matched pattern.`);
    }
  } 

  else if (command.startsWith("add to the file ")) {
    const match = command.match(/^add to the file (.+) this content: (.*)$/s);
    if (match) {
      await addToFile(match[1].trim(), match[2].trim());
    } else {
      console.log(`⚠️ there is no matched pattern.`);
    }
  }
};

const createFile = async (path) => {
  if (await fileExist(path)) {
    return console.log(`${path} file does exist.`);
  }

  try {
    await writeFile(path, "");
    console.log(`✅ ${path} is created.`);
  } catch (error) {
    console.log(`❌ 에러 발생 : ${error.message}`);
  }
};

const deleteFile = async (path) => {
  try {
    await unlink(path);
    console.log(`🗑️ ${path} is deleted successfully`);
  } catch (error) {
    if (error.code === "ENOENT") {
      console.log(`⚠️ ${path} does not exist`);
    }
    console.error(`❌ 에러 발생: ${error.message}`);
  }
};

const renameFile = async (oldPath, newPath) => {
  try {
    await rename(oldPath, newPath);
    console.log(`✅ ${oldPath} is now ${newPath} successfully`);
  } catch (error) {
    if (error.code === "ENOENT") {
      console.log(`⚠️ ${oldPath} does not exist`);
    }
    console.error(`❌ 에러 발생: ${error.message}`);
  }
};

let previousContent = ""; // 중복 실행(이벤트 바운스) 방지용 상태 변수

const addToFile = async (path, content) => {
  // 1차 방어: 동일한 내용이 연속으로 오면 무시 (이벤트 디바운싱 효과)
  if (previousContent === content) return;

  // 2차 방어: 의도치 않은 새 파일 생성을 원천 차단 (Early Return)
  if (!(await fileExist(path))) {
    console.log(
      `⚠️ 경고: '${path}' 파일이 존재하지 않아 내용을 추가할 수 없습니다.`,
    );
    return;
  }

  try {
    await appendFile(path, content);
    previousContent = content;
    console.log(`📝 내용이 '${path}'에 성공적으로 추가되었습니다.`);
  } catch (e) {
    // fileExist로 거르지 못한 OS 레벨의 치명적 예외 상황 처리
    console.error(`❌ 파일에 내용 추가 중 치명적 오류 발생: ${e.message}`);
  }
};

if (!(await fileExist(FILE_COMMANDS))) {
  await writeFile(FILE_COMMANDS, "");
}

console.log(`👀 ${FILE_COMMANDS} 파일 변경 사항을 감시하는 중`);

process.on('SIGINT', async () => {
  console.log('\n👋 종료 신호 감지, 정리 중...');
  // watcher 닫기, 파일 핸들 닫기 등 cleanup
  process.exit(0);
});

try {
  const watcher = watch(FILE_COMMANDS); // async iterator object 반환

  for await (const event of watcher) {
    // for await of 는 main thread를 blocking 하지 않음
    if (event.eventType === "change") {
      console.log(
        `[알림] ${event.filename} 파일에서 ${event.eventType} 이벤트 발생! ${new Date().toISOString()}`,
      );
      // Event Emitter 에서부터 발생하는 개념
      const fileHandler = await open(FILE_COMMANDS, "r"); // 파일 디스크립터(FD)

      try {
        const stat = await fileHandler.stat();
        const size = stat.size;

        if (size === 0) continue;

        const buff = Buffer.alloc(size);
        const offset = 0;
        const length = buff.byteLength;
        const position = 0;

        await fileHandler.read(buff, offset, length, position);
        const commandString = buff.toString("utf8");

        await executeCommand(commandString);
      } finally {
        await fileHandler.close(); // 메모리 누수 방지 fd 파일 디스크립터 반납 EMFILE에러 방지
      }
    }
  }
} catch (error) {
  console.error(`❌ 에러 발생 : ${error.message}`);
}