Node.js로 사진 폴더 정리 기능 만들기

profile
마릴린벅시
2023. 11. 13. 23:07백엔드
반응형

 

시나리오

나는 Pictures라는 폴더에 사진과 영상을 저장한다. 이 사진과 영상을 달 별로 정리한 뒤,

달 별로 정리된 폴더 안에서 video파일들은 video폴더에,

아이폰 캡쳐 시 생성되는 aae, png확장자 파일들은 captured폴더에,

보정을 한 사진의 경우 보정 원본 사진은 duplicated폴더에 나눠 정리하고싶다.

참고로 아이폰에서는 사진을 보정하면 원래이름에 IMG_0710 → IMG_E0710 으로 변경된다.

명령어는 node [정리할폴더명] [사용할폴더명] 로 실행되도록 만들자.

 

요구사항

  1. 명령어로 target폴더를 받아, 그 폴더의 사진들을 읽어오기
  2. 읽어온 파일들의 날짜 정보를 읽어, 달 별로 나누어 넣기
  3. 달 별로 나눠진 폴더를 읽어, 각 폴더에 video, captured, duplicated 폴더 생성하기
  4. video, captured, duplicated폴더에 각각 알맞는 파일들을 옮기기
  5. 명령어 'node app MyPicture' 로 실행하면 Pictures/MyPictures 폴더의 사진과 비디오가 자동으로 정리 된다.

 

 

코드

const path = require('path');
const os = require('os');
const fs = require('fs');

// 1. 명령어로 target폴더를 받아, 그 폴더의 사진들을 읽어오기
const folder = process.argv?.[2] ?? '';
const workingDir = path.join(os.homedir(), 'Pictures', folder);

if (!folder || !fs.existsSync(workingDir)) {
  console.error('Please enter folder name in Pictures');
}

// 2. 읽어온 파일들의 날짜 정보를 읽어, 달 별로 나누어 넣기
try {
  const files = fs.readdirSync(workingDir);
  if (files.length) processFiles(files);
} catch (e) {
  console.error(e);
}

function processFiles(files) {
  files.forEach((file) => {
    const stat = fs.statSync(path.join(workingDir, file)); //
    organizeByMonth(file, stat);
  });
}

function organizeByMonth(file, stat) {
  if (!stat.isFile() || file.startsWith('.')) return;
  const date = stat.mtime;
  const year = date.getFullYear();
  const month = date.getMonth();

  const monthName = new Intl.DateTimeFormat('ko-KR', { month: 'short' }).format(
    new Date(year, month, 1)
  );

  const monthDir = path.join(workingDir, monthName);
  !fs.existsSync(monthDir) && fs.mkdirSync(monthDir);
  fs.renameSync(path.join(workingDir, file), path.join(monthDir, file));
}

// 3. 달 별로 나눠진 폴더를 읽어, 각 폴더에 video, captured, duplicated 폴더 생성하기
try {
  const files = fs.readdirSync(workingDir);
  if (files.length) processMonthDir(files);
} catch (e) {
  console.error(e);
}

function processMonthDir(files) {
  files.forEach((file) => {
    const isDirectory = fs.statSync(path.join(workingDir, file)).isDirectory();
    if (!isDirectory || file.startsWith('.')) return;
    const videoDir = path.join(workingDir, file, 'video');
    const capturedDir = path.join(workingDir, file, 'captured');
    const duplicatedDir = path.join(workingDir, file, 'duplicated');

    !fs.existsSync(videoDir) && fs.mkdirSync(videoDir);
    !fs.existsSync(capturedDir) && fs.mkdirSync(capturedDir);
    !fs.existsSync(duplicatedDir) && fs.mkdirSync(duplicatedDir);
  });
}

// 4. video, captured, duplicated폴더에 각각 알맞는 파일들을 옮기기
try {
  const files = fs.readdirSync(workingDir);
  if (files.length) organizeByFileExt(files);
} catch (e) {
  console.error(e);
}

function organizeByFileExt(dirs) {
  dirs.forEach((dir) => {
    const dirPath = path.join(workingDir, dir);
    fs.readdir(dirPath, (err, files) => {
      if (!files) return;
      files.forEach((file) => {
        const stat = fs.statSync(path.join(dirPath, file));
        if (!stat.isFile) return;

        const videoDir = path.join(dirPath, 'video');
        const capturedDir = path.join(dirPath, 'captured');
        const duplicatedDir = path.join(dirPath, 'duplicated');

        if (isVideoFile(file)) {
          move(file, dirPath, videoDir);
        } else if (isCapturedFile(file)) {
          move(file, dirPath, capturedDir);
        } else if (isDuplicatedFile(files, file)) {
          move(file, dirPath, duplicatedDir);
        }
      });
    });
  });
}

function isVideoFile(file) {
  const regExp = /(mp4|mov)$/gm;
  const match = file.toLowerCase().match(regExp);
  return !!match;
}
function isCapturedFile(file) {
  const regExp = /(png|aae)$/gm;
  const match = file.toLowerCase().match(regExp);
  return !!match;
}
function isDuplicatedFile(files, file) {
  if (!file.startsWith('IMG_') || file.startsWith('IMG_E')) {
    return false;
  }

  const edited = `IMG_E${file.split('_')[1]}`;
  const found = files.find((f) => f.includes(edited));
  return !!found;
}

function move(file, sourceDir, targetDir) {
  const oldPath = path.join(sourceDir, file);
  const newPath = path.join(targetDir, file);
  fs.promises //
    .rename(oldPath, newPath)
    .catch(console.error);
}

 

 

결과물

 

 

 

느낀점

월별 폴더를 생성하면서 날짜별로 파일을 이동시키고, 월별 폴더 안에 카테고리 별 폴더를 만들고, 그 다음 파일들을 이동하는 과정이 순차적으로 진행되어야 했다.

노드에서 제공하는 fs모듈의 api는 같은 기능을 하더라도 sync, callback을 받는 형태, promise 이렇게 3가지 형태로 만들어져 있다.

처음에 비동기 함수를 사용하면서 월별 폴더 생성까지만 정상적으로 수행되고, 카테고리 별 폴더 생성이 되지 않는 부분에서 계속 삽질을 하다가 나중에야 아! 하고 깨달았다.

첫 readdir에서 월별로 파일이 정리되기도 전에 다음 readdir이 수행되어 버려서, 유효한 path가 없기 때문에 그 다음 작업이 모두 되지 않았던 것이다. 

프론트에서는 비동기작업을 할 때 데이터가 잘 받아와졌는지 아닌지를 예측하는게 가장 중요한데, 만약 데이터가 준비되지 않았을 경우에는 초기 데이터를 넣어두거나, 자바스크립트의 '??'연산자 등을 사용해서 대응을 할 순 있었다.

하지만 백엔드에는 데이터를 생성하거나 옮길 때 어떤 작업이 누락 될 위험이 훨씬 클 수 있겠다는 생각이 들어 아찔했다. 

프론트는 프론트대로의 재미가 있는데 노드로는 이렇게 직접 파일을 읽고 옮길 수도 있고, 내 컴퓨터의 os정보도 읽어올 수 있는 등 새로운 재미가 있는 것 같다.