Flutter_Dart/Rust

[rust] 확장자별로 폴더 생성, 파일 이동

2025. 10. 14.



카메라에서 추출한 파일을 사진과 영상으로 나누기 위한 간단한 프로그램을 이전에 파이썬으로 만든 적이 있다. 아무래도 윈도우 기반 프로그램이다 보니, rust로 다시 만들어 본다.

 

 

 

[파이썬] 확장자별로 폴더 생성, 파일 이동 (2)

소니카메라에서 추출한 파일을 백업하기 위해 바탕화면에 옮기다보면, arw, jpg, mov 파일이 뒤죽박죽 섞여있는데, 일일이 옮기기 귀찮은 경우가 있다. 그래서 과거에 확장자별로 폴더를 이동하는

sunnybong.tistory.com

 

 

프로그램을 쓰다보니, 구지, 이미지와 영상을 가르기 보다는 각각의 폴더를 만드는 것도 나쁘지 않을 것 같다.

chatgpt에 의뢰하고, 완료하는데까지 5분이 채 걸리지 않은 것 같다. 세상이 정말 바뀌긴 했구나..

 

 

빌드 후 exe 파일을 실행하면, 폴더 선택창이 나오는데, 원하는 폴더를 선택하기만 하면 끝이다. 폴더는 무시되고, 파일은 확장자별로 이동이 된다.

 

폴더를 잘못 선택하면 원치 않는 무시무시한 결과가 나타나니 주의해야한다.

디폴트 폴더를 사진이 원하는 폴더로 지정해두면 더 나을 듯하다.

 

 

코드는 아래와 같다.

 

// 주석 해제하면 콘솔 창 없이 실행됩니다 (Windows 전용 GUI 서브시스템).
// #![windows_subsystem = "windows"]

use anyhow::{Context, Result};
use std::{
    collections::HashMap,
    ffi::OsStr,
    fs,
    path::{Path, PathBuf},
};

fn main() {
    if let Err(e) = run() {
        // 문제가 생기면 메시지 박스로 알려줍니다.
        let _ = rfd::MessageDialog::new()
            .set_title("오류")
            .set_description(format!("{e:#}"))
            .set_level(rfd::MessageLevel::Error)
            .show();
        // 콘솔 빌드일 경우를 위해서도 에러 출력
        eprintln!("{e:#}");
    }
}

fn run() -> Result<()> {
    // 1) 폴더 선택 대화상자
    let maybe_dir = rfd::FileDialog::new()
        .set_title("정리할 폴더를 선택하세요")
        .pick_folder();

    let dir = match maybe_dir {
        Some(d) => d,
        None => return Ok(()), // 사용자가 취소
    };

    // 2) 선택한 폴더 내의 파일만 순회 (하위 폴더는 건드리지 않음)
    let mut moved: usize = 0;
    let mut skipped: usize = 0;
    let mut by_ext: HashMap<String, usize> = HashMap::new();

    for entry in fs::read_dir(&dir).with_context(|| format!("폴더 접근 실패: {}", dir.display()))?
    {
        let entry = entry?;
        let path = entry.path();

        // 디렉터리는 스킵
        let meta = match entry.metadata() {
            Ok(m) => m,
            Err(_) => {
                skipped += 1;
                continue;
            }
        };
        if !meta.is_file() {
            continue;
        }

        // 3) 확장자 추출 (없으면 "no_extension")
        let ext = ext_key(path.as_path());

        // 4) 대상 폴더 생성
        let target_dir = dir.join(&ext);
        fs::create_dir_all(&target_dir)
            .with_context(|| format!("하위 폴더 생성 실패: {}", target_dir.display()))?;

        // 5) 파일 이동 (이름 충돌 시 _1, _2 …를 붙여 고유화)
        let file_name = match path.file_name() {
            Some(n) => n.to_owned(),
            None => {
                skipped += 1;
                continue;
            }
        };
        let mut target_path = target_dir.join(&file_name);
        target_path = ensure_unique_path(&target_path);

        match fs::rename(&path, &target_path) {
            Ok(_) => {
                moved += 1;
                *by_ext.entry(ext).or_insert(0) += 1;
            }
            Err(_) => {
                // 오류 종류와 무관하게 copy + remove 로 폴백
                fs::copy(&path, &target_path).with_context(|| {
                    format!(
                        "파일 복사 실패: {} → {}",
                        path.display(),
                        target_path.display()
                    )
                })?;
                fs::remove_file(&path)
                    .with_context(|| format!("원본 삭제 실패: {}", path.display()))?;
                moved += 1;
                *by_ext.entry(ext).or_insert(0) += 1;
            }
        }
    }

    // 6) 결과 요약 메시지
    let mut lines = vec![
        format!("정리 대상: {}", dir.display()),
        format!("이동한 파일: {moved}개"),
        format!("스킵: {skipped}개"),
        "".into(),
        "확장자별 이동 결과:".into(),
    ];
    if by_ext.is_empty() {
        lines.push("- (이동된 파일 없음)".into());
    } else {
        let mut items: Vec<_> = by_ext.into_iter().collect();
        // 보기 좋게 정렬 (개수 내림차순, 그다음 확장자)
        items.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
        for (ext, cnt) in items {
            lines.push(format!("  • {}: {}개", ext, cnt));
        }
    }

    let _ = rfd::MessageDialog::new()
        .set_title("정리 완료")
        .set_description(lines.join("\n"))
        .set_level(rfd::MessageLevel::Info)
        .show();

    Ok(())
}

/// 파일의 확장자를 소문자로 정규화하여 폴더명으로 사용할 키를 생성.
/// 확장자가 없으면 "no_extension"을 반환.
fn ext_key(path: &Path) -> String {
    path.extension()
        .and_then(OsStr::to_str)
        .map(|s| s.trim_matches('.').to_lowercase())
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| "no_extension".to_string())
}

/// 대상 경로가 이미 존재하면 뒤에 `_1`, `_2`…를 붙여서 충돌을 피함.
fn ensure_unique_path(path: &Path) -> PathBuf {
    if !path.exists() {
        return path.to_path_buf();
    }
    let base = path.to_path_buf(); // mut 제거
    let dir = base
        .parent()
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| PathBuf::from("."));
    let stem = base
        .file_stem()
        .and_then(OsStr::to_str)
        .unwrap_or("file")
        .to_string();
    let ext = base.extension().and_then(OsStr::to_str);

    let mut idx: u32 = 1;
    loop {
        let mut candidate = dir.join(format!("{stem}_{idx}"));
        if let Some(e) = ext {
            candidate.set_extension(e);
        }
        if !candidate.exists() {
            return candidate;
        }
        idx += 1;
    }
}

 

 

 

아무래도 파이썬 파일에 비해 용량이 매우 작다는 것이 고무적인 일이다. 시간이 될때 코드를 찬찬히 살펴봐야겠다.

 

끝.

 

 

 

ext-sorter.zip
0.11MB

728x90