tidy-file-organizer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +47 -0
- data/.rspec +1 -0
- data/.rubocop.cookpad-styleguide.yml +373 -0
- data/.rubocop.yml +25 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +102 -0
- data/README.ja.md +222 -0
- data/README.md +222 -0
- data/config/default.ja.yml +92 -0
- data/config/default.yml +92 -0
- data/exe/tidyify +5 -0
- data/lib/tidy_file_organizer/cli.rb +99 -0
- data/lib/tidy_file_organizer/config.rb +122 -0
- data/lib/tidy_file_organizer/date_organizer.rb +72 -0
- data/lib/tidy_file_organizer/duplicate_detector.rb +197 -0
- data/lib/tidy_file_organizer/duplicate_display.rb +104 -0
- data/lib/tidy_file_organizer/file_helper.rb +57 -0
- data/lib/tidy_file_organizer/file_mover.rb +57 -0
- data/lib/tidy_file_organizer/i18n.rb +43 -0
- data/lib/tidy_file_organizer/locale/en.yml +110 -0
- data/lib/tidy_file_organizer/locale/ja.yml +110 -0
- data/lib/tidy_file_organizer/organizer.rb +117 -0
- data/lib/tidy_file_organizer/post_install.rb +31 -0
- data/lib/tidy_file_organizer/setup_prompt.rb +194 -0
- data/lib/tidy_file_organizer/version.rb +3 -0
- data/lib/tidy_file_organizer.rb +15 -0
- data/tidy-file-organizer.gemspec +49 -0
- metadata +147 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module TidyFileOrganizer
|
|
2
|
+
module FileHelper
|
|
3
|
+
BYTE_UNITS = %w[B KB MB GB TB].freeze
|
|
4
|
+
BYTES_PER_UNIT = 1024.0
|
|
5
|
+
|
|
6
|
+
# Collect files
|
|
7
|
+
def collect_files(target_dir, recursive: false, exclude_dirs: [])
|
|
8
|
+
if recursive
|
|
9
|
+
collect_files_recursively(target_dir, exclude_dirs)
|
|
10
|
+
else
|
|
11
|
+
collect_files_in_directory(target_dir)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Get relative path
|
|
16
|
+
def relative_path(file_path, base_dir)
|
|
17
|
+
file_path.sub("#{base_dir}/", '')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Convert file size to human-readable format
|
|
21
|
+
def human_readable_size(size)
|
|
22
|
+
unit_index = 0
|
|
23
|
+
size_float = size.to_f
|
|
24
|
+
|
|
25
|
+
while size_float >= BYTES_PER_UNIT && unit_index < BYTE_UNITS.size - 1
|
|
26
|
+
size_float /= BYTES_PER_UNIT
|
|
27
|
+
unit_index += 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
format('%.2f %s', size_float, BYTE_UNITS[unit_index])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def collect_files_in_directory(target_dir)
|
|
36
|
+
Dir.children(target_dir)
|
|
37
|
+
.map { |entry| File.join(target_dir, entry) }
|
|
38
|
+
.select { |path| File.file?(path) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def collect_files_recursively(target_dir, exclude_dirs)
|
|
42
|
+
pattern = File.join(target_dir, '**', '*')
|
|
43
|
+
Dir.glob(pattern).select do |path|
|
|
44
|
+
File.file?(path) && !excluded_path?(path, target_dir, exclude_dirs)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def excluded_path?(path, target_dir, exclude_dirs)
|
|
49
|
+
return false if exclude_dirs.empty?
|
|
50
|
+
|
|
51
|
+
relative = path.sub("#{target_dir}/", '')
|
|
52
|
+
path_parts = relative.split('/')
|
|
53
|
+
|
|
54
|
+
exclude_dirs.any? { |dir| path_parts.include?(dir) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
|
|
3
|
+
module TidyFileOrganizer
|
|
4
|
+
class FileMover
|
|
5
|
+
attr_reader :target_dir
|
|
6
|
+
|
|
7
|
+
def initialize(target_dir)
|
|
8
|
+
@target_dir = File.expand_path(target_dir)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Move file to specified directory
|
|
12
|
+
def move_file(file_path, dest_dir_name, dry_run: false)
|
|
13
|
+
filename = File.basename(file_path)
|
|
14
|
+
relative = relative_path(file_path)
|
|
15
|
+
dest_dir = File.join(@target_dir, dest_dir_name)
|
|
16
|
+
dest_path = File.join(dest_dir, filename)
|
|
17
|
+
|
|
18
|
+
return handle_conflict(relative, dest_dir_name, dry_run) if conflicting?(file_path, dest_path)
|
|
19
|
+
return handle_skip(relative, dry_run) if already_in_place?(file_path, dest_path)
|
|
20
|
+
|
|
21
|
+
perform_move(file_path, dest_dir, dest_path, relative, dest_dir_name, dry_run)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def relative_path(file_path)
|
|
27
|
+
file_path.sub("#{@target_dir}/", '')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def conflicting?(file_path, dest_path)
|
|
31
|
+
File.exist?(dest_path) && file_path != dest_path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def already_in_place?(file_path, dest_path)
|
|
35
|
+
file_path == dest_path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def handle_conflict(relative, dest_dir_name, dry_run)
|
|
39
|
+
message = "⚠️ Conflict: #{relative} -> #{dest_dir_name}/ (filename conflict)"
|
|
40
|
+
puts dry_run ? "[Dry-run] #{message}" : message
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def handle_skip(relative, dry_run)
|
|
44
|
+
puts I18n.t('organizer.skip', file: relative) if dry_run
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def perform_move(file_path, dest_dir, dest_path, relative, dest_dir_name, dry_run)
|
|
48
|
+
if dry_run
|
|
49
|
+
puts "[Dry-run] #{relative} -> #{dest_dir_name}/"
|
|
50
|
+
else
|
|
51
|
+
FileUtils.mkdir_p(dest_dir)
|
|
52
|
+
FileUtils.mv(file_path, dest_path)
|
|
53
|
+
puts "Moved: #{relative} -> #{dest_dir_name}/"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
|
|
3
|
+
module TidyFileOrganizer
|
|
4
|
+
module I18n
|
|
5
|
+
class << self
|
|
6
|
+
def locale
|
|
7
|
+
@locale ||= detect_locale
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def locale=(value)
|
|
11
|
+
@locale = value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def t(key, **options)
|
|
15
|
+
translations = load_translations(locale)
|
|
16
|
+
text = translations.dig(*key.to_s.split('.')) || key.to_s
|
|
17
|
+
|
|
18
|
+
# Variable substitution
|
|
19
|
+
options.each do |k, v|
|
|
20
|
+
text = text.gsub("%{#{k}}", v.to_s)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
text
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def detect_locale
|
|
29
|
+
lang = ENV['LANG'].to_s
|
|
30
|
+
lang.start_with?('ja') ? :ja : :en
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def load_translations(locale)
|
|
34
|
+
@translations ||= {}
|
|
35
|
+
@translations[locale] ||= begin
|
|
36
|
+
locale_file = File.expand_path("locale/#{locale}.yml", __dir__)
|
|
37
|
+
yaml_data = YAML.load_file(locale_file)
|
|
38
|
+
yaml_data[locale.to_s]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
en:
|
|
2
|
+
setup:
|
|
3
|
+
title: tidy-file-organizer Setup
|
|
4
|
+
separator: "============================================================"
|
|
5
|
+
section_separator: "------------------------------------------------------------"
|
|
6
|
+
target_directory: "Target directory: %{dir}"
|
|
7
|
+
language_setting: "[0] Folder Name Language Setting"
|
|
8
|
+
language_description: Choose the language for organized folder names
|
|
9
|
+
language_option_1: " 1: English (e.g., Images, Documents, Screenshots)"
|
|
10
|
+
language_option_2: " 2: Japanese (e.g., 画像, 書類, スクリーンショット)"
|
|
11
|
+
current_setting: "Current setting: %{setting}"
|
|
12
|
+
language_prompt: "Choose (1=English, 2=Japanese, Enter=keep current): "
|
|
13
|
+
invalid_input: Invalid input. Using default (English).
|
|
14
|
+
extensions_title: "[1] File Extension Rules"
|
|
15
|
+
extensions_description: Specify destination folders based on file extensions
|
|
16
|
+
keywords_title: "[2] Keyword Rules"
|
|
17
|
+
keywords_description: Specify destination folders based on keywords in filenames
|
|
18
|
+
keywords_note: "※Keywords have priority over extensions"
|
|
19
|
+
input_format: "Input format: extension_list:folder_name extension_list:folder_name ..."
|
|
20
|
+
default_values: "Default values:"
|
|
21
|
+
current_config: "Current config: %{config}"
|
|
22
|
+
new_config_prompt: "Enter new config (Enter to use defaults): "
|
|
23
|
+
config_saved: "✓ Configuration saved"
|
|
24
|
+
save_location: " Location: %{path}"
|
|
25
|
+
next_steps: "Next steps:"
|
|
26
|
+
step_dry_run: " 1. Dry-run: tidyify run %{dir} --dry-run"
|
|
27
|
+
step_execute: " 2. Execute: tidyify run %{dir}"
|
|
28
|
+
none: none
|
|
29
|
+
english: English
|
|
30
|
+
japanese: Japanese
|
|
31
|
+
|
|
32
|
+
organizer:
|
|
33
|
+
no_config: Configuration not found. Please run 'setup' command first.
|
|
34
|
+
starting: "--- Starting organization (%{dir}) %{mode} ---"
|
|
35
|
+
dry_run_mode: "[Dry-run mode]"
|
|
36
|
+
recursive_mode: "[Recursive mode]"
|
|
37
|
+
moved: "Moved: %{file} -> %{dest}"
|
|
38
|
+
dry_run_moved: "[Dry-run] %{file} -> %{dest}"
|
|
39
|
+
cleaned_up: "Cleaned up: %{dir} (removed empty directory)"
|
|
40
|
+
no_files: No files to organize.
|
|
41
|
+
completed: Organization completed.
|
|
42
|
+
skip: "[Skip] %{file} (already in correct location)"
|
|
43
|
+
|
|
44
|
+
date_organizer:
|
|
45
|
+
starting: "--- Starting date-based organization (%{dir}) %{mode} ---"
|
|
46
|
+
pattern: "Pattern: %{pattern}"
|
|
47
|
+
invalid_pattern: "Invalid pattern: %{pattern}. Use year, year-month, or year-month-day."
|
|
48
|
+
|
|
49
|
+
duplicate_detector:
|
|
50
|
+
starting: "--- Starting duplicate detection (%{dir}) ---"
|
|
51
|
+
file_count: "File count: %{count}"
|
|
52
|
+
calculating: Calculating hashes...
|
|
53
|
+
progress: "Progress: %{current}/%{total}"
|
|
54
|
+
result_title: "=== Duplicate Detection Results ==="
|
|
55
|
+
duplicate_groups: "Duplicate groups: %{count}"
|
|
56
|
+
duplicate_files: "Duplicate files: %{count}"
|
|
57
|
+
no_duplicates: No duplicates found.
|
|
58
|
+
group_title: "--- Group %{num} (%{count} files, hash: %{hash}...) ---"
|
|
59
|
+
file_size: "File size: %{size}"
|
|
60
|
+
wasted_space: "Wasted space: %{size}"
|
|
61
|
+
total_wasted: "Total wasted space: %{size}"
|
|
62
|
+
deletion_starting: "--- Starting duplicate deletion %{mode} ---"
|
|
63
|
+
deletion_plan: "--- Deletion Plan ---"
|
|
64
|
+
will_keep: "Keep: %{file}"
|
|
65
|
+
will_delete: "Delete: %{file} (%{size})"
|
|
66
|
+
kept: "Keep: %{file}"
|
|
67
|
+
deleted: "Delete: %{file} (%{size})"
|
|
68
|
+
confirm_deletion: "Delete these files? [yes/no]: "
|
|
69
|
+
deletion_cancelled: Deletion cancelled.
|
|
70
|
+
invalid_response: Invalid response. Please enter yes or no.
|
|
71
|
+
summary: "--- Summary ---"
|
|
72
|
+
deleted_count: "Deleted files: %{count}"
|
|
73
|
+
saved_space: "Saved disk space: %{size}"
|
|
74
|
+
confirm_header: Duplicate File Deletion Confirmation
|
|
75
|
+
confirm_separator: "============================================================"
|
|
76
|
+
files_to_delete: "Files to delete: %{count}"
|
|
77
|
+
space_to_save: "Disk space to save: %{size}"
|
|
78
|
+
files_list_title: "Files to delete:"
|
|
79
|
+
kept_file: " Kept file: %{file}"
|
|
80
|
+
executing_deletion: Executing deletion...
|
|
81
|
+
|
|
82
|
+
cli:
|
|
83
|
+
error_directory_required: "Error: Please specify target directory"
|
|
84
|
+
usage: "Usage: tidyify [command] [target_directory] [options]"
|
|
85
|
+
commands: "Commands:"
|
|
86
|
+
cmd_setup: " setup Set up organization rules interactively (defaults to current dir)"
|
|
87
|
+
cmd_run: " run Organize files based on configuration (defaults to current dir)"
|
|
88
|
+
cmd_organize_date: " organize-by-date Organize files by modification date"
|
|
89
|
+
cmd_find_dup: " find-duplicates Find duplicate files"
|
|
90
|
+
cmd_remove_dup: " remove-duplicates Remove duplicate files (keep first one)"
|
|
91
|
+
options: "Options:"
|
|
92
|
+
opt_dry_run: " --dry-run Simulate without actual execution"
|
|
93
|
+
opt_recursive: " --recursive, -r Process files in subdirectories recursively"
|
|
94
|
+
opt_pattern: " --pattern=<pattern> Date pattern (year, year-month, year-month-day)"
|
|
95
|
+
opt_no_confirm: " --no-confirm Skip confirmation before deletion (remove-duplicates only)"
|
|
96
|
+
examples: "Examples:"
|
|
97
|
+
ex_setup: |2
|
|
98
|
+
tidyify setup # Set up current directory
|
|
99
|
+
tidyify setup ~/Downloads # Set up specific directory
|
|
100
|
+
ex_run_current: " tidyify run # Organize current directory"
|
|
101
|
+
ex_run_dry: " tidyify run ~/Downloads --dry-run # Dry-run"
|
|
102
|
+
ex_run_exec: " tidyify run ~/Downloads --recursive # Execute"
|
|
103
|
+
ex_organize_date: " tidyify organize-by-date ~/Downloads --pattern=year-month"
|
|
104
|
+
ex_find_dup: " tidyify find-duplicates ~/Downloads --recursive"
|
|
105
|
+
ex_remove_dup: " tidyify remove-duplicates ~/Downloads --recursive"
|
|
106
|
+
ex_remove_no_confirm: " tidyify remove-duplicates ~/Downloads --no-confirm"
|
|
107
|
+
|
|
108
|
+
post_install:
|
|
109
|
+
created_default_en: "✓ Created default config file (English): %{path}"
|
|
110
|
+
created_default_ja: "✓ Created default config file (Japanese): %{path}"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
ja:
|
|
2
|
+
setup:
|
|
3
|
+
title: tidy-file-organizer セットアップ
|
|
4
|
+
separator: "============================================================"
|
|
5
|
+
section_separator: "------------------------------------------------------------"
|
|
6
|
+
target_directory: "対象ディレクトリ: %{dir}"
|
|
7
|
+
language_setting: "[0] フォルダ名の言語設定"
|
|
8
|
+
language_description: 整理先フォルダ名を日本語にするか英語にするか選択します
|
|
9
|
+
language_option_1: " 1: English (例: Images, Documents, Screenshots)"
|
|
10
|
+
language_option_2: " 2: 日本語 (例: 画像, 書類, スクリーンショット)"
|
|
11
|
+
current_setting: "現在の設定: %{setting}"
|
|
12
|
+
language_prompt: "選択 (1=English, 2=日本語, Enter=現在の設定のまま): "
|
|
13
|
+
invalid_input: 無効な入力です。デフォルト(English)を使用します。
|
|
14
|
+
extensions_title: "[1] 拡張子による整理ルール"
|
|
15
|
+
extensions_description: ファイルの拡張子に基づいて整理先フォルダを指定します
|
|
16
|
+
keywords_title: "[2] キーワードによる整理ルール"
|
|
17
|
+
keywords_description: ファイル名に含まれるキーワードで整理先フォルダを指定します
|
|
18
|
+
keywords_note: "※キーワードは拡張子より優先されます"
|
|
19
|
+
input_format: "入力形式: 拡張子リスト:フォルダ名 拡張子リスト:フォルダ名 ..."
|
|
20
|
+
default_values: "デフォルト値:"
|
|
21
|
+
current_config: "現在の設定: %{config}"
|
|
22
|
+
new_config_prompt: "新しい設定を入力 (デフォルト値を使う場合はEnter): "
|
|
23
|
+
config_saved: "✓ 設定を保存しました"
|
|
24
|
+
save_location: " 保存先: %{path}"
|
|
25
|
+
next_steps: "次のステップ:"
|
|
26
|
+
step_dry_run: " 1. シミュレーション: tidyify run %{dir} --dry-run"
|
|
27
|
+
step_execute: " 2. 実際に整理を実行: tidyify run %{dir}"
|
|
28
|
+
none: なし
|
|
29
|
+
english: English
|
|
30
|
+
japanese: 日本語
|
|
31
|
+
|
|
32
|
+
organizer:
|
|
33
|
+
no_config: 設定が見つかりません。先に 'setup' コマンドを実行してください。
|
|
34
|
+
starting: "--- 整理を開始します (%{dir}) %{mode} ---"
|
|
35
|
+
dry_run_mode: "[Dry-run モード]"
|
|
36
|
+
recursive_mode: "[再帰モード]"
|
|
37
|
+
moved: "Moved: %{file} -> %{dest}"
|
|
38
|
+
dry_run_moved: "[Dry-run] %{file} -> %{dest}"
|
|
39
|
+
cleaned_up: "Cleaned up: %{dir} (空ディレクトリを削除)"
|
|
40
|
+
no_files: 整理対象のファイルが見つかりませんでした。
|
|
41
|
+
completed: 整理が完了しました。
|
|
42
|
+
skip: "[Skip] %{file} (既に正しい場所にあります)"
|
|
43
|
+
|
|
44
|
+
date_organizer:
|
|
45
|
+
starting: "--- 日付ベースの整理を開始します (%{dir}) %{mode} ---"
|
|
46
|
+
pattern: "整理パターン: %{pattern}"
|
|
47
|
+
invalid_pattern: "無効なパターン: %{pattern}。year, year-month, year-month-day のいずれかを使用してください。"
|
|
48
|
+
|
|
49
|
+
duplicate_detector:
|
|
50
|
+
starting: "--- 重複ファイルの検出を開始します (%{dir}) ---"
|
|
51
|
+
file_count: "ファイル数: %{count}"
|
|
52
|
+
calculating: ハッシュ値を計算中...
|
|
53
|
+
progress: "進捗: %{current}/%{total}"
|
|
54
|
+
result_title: "=== 重複ファイルの検出結果 ==="
|
|
55
|
+
duplicate_groups: "重複グループ数: %{count}"
|
|
56
|
+
duplicate_files: "重複ファイル数: %{count}"
|
|
57
|
+
no_duplicates: 重複ファイルは見つかりませんでした。
|
|
58
|
+
group_title: "--- グループ %{num} (%{count} 件, ハッシュ: %{hash}...) ---"
|
|
59
|
+
file_size: "ファイルサイズ: %{size}"
|
|
60
|
+
wasted_space: "無駄な容量: %{size}"
|
|
61
|
+
total_wasted: "合計無駄容量: %{size}"
|
|
62
|
+
deletion_starting: "--- 重複ファイルの削除を開始します %{mode} ---"
|
|
63
|
+
deletion_plan: "--- 削除計画 ---"
|
|
64
|
+
will_keep: "保持: %{file}"
|
|
65
|
+
will_delete: "削除: %{file} (%{size})"
|
|
66
|
+
kept: "保持: %{file}"
|
|
67
|
+
deleted: "削除: %{file} (%{size})"
|
|
68
|
+
confirm_deletion: "これらのファイルを削除しますか? [yes/no]: "
|
|
69
|
+
deletion_cancelled: 削除をキャンセルしました。
|
|
70
|
+
invalid_response: 無効な応答です。yes か no を入力してください。
|
|
71
|
+
summary: "--- サマリー ---"
|
|
72
|
+
deleted_count: "削除されたファイル数: %{count}"
|
|
73
|
+
saved_space: "節約されたディスク容量: %{size}"
|
|
74
|
+
confirm_header: 重複ファイル削除の確認
|
|
75
|
+
confirm_separator: "============================================================"
|
|
76
|
+
files_to_delete: "削除対象のファイル数: %{count}"
|
|
77
|
+
space_to_save: "節約されるディスク容量: %{size}"
|
|
78
|
+
files_list_title: "削除対象のファイル:"
|
|
79
|
+
kept_file: " 保持されるファイル: %{file}"
|
|
80
|
+
executing_deletion: 削除を実行します...
|
|
81
|
+
|
|
82
|
+
cli:
|
|
83
|
+
error_directory_required: "エラー: 対象ディレクトリを指定してください"
|
|
84
|
+
usage: "Usage: tidyify [command] [target_directory] [options]"
|
|
85
|
+
commands: "Commands:"
|
|
86
|
+
cmd_setup: " setup 整理ルールをインタラクティブに設定します(ディレクトリ省略時はカレントディレクトリ)"
|
|
87
|
+
cmd_run: " run 設定に基づいてファイルを整理します(ディレクトリ省略時はカレントディレクトリ)"
|
|
88
|
+
cmd_organize_date: " organize-by-date ファイルを更新日時ベースで整理します"
|
|
89
|
+
cmd_find_dup: " find-duplicates 重複ファイルを検出します"
|
|
90
|
+
cmd_remove_dup: " remove-duplicates 重複ファイルを削除します(最初のファイルを保持)"
|
|
91
|
+
options: "Options:"
|
|
92
|
+
opt_dry_run: " --dry-run 実際には実行せず、シミュレーションのみ行います"
|
|
93
|
+
opt_recursive: " --recursive, -r サブディレクトリ内のファイルも再帰的に処理します"
|
|
94
|
+
opt_pattern: " --pattern=<pattern> 日付整理のパターン (year, year-month, year-month-day)"
|
|
95
|
+
opt_no_confirm: " --no-confirm 削除前の確認をスキップします(remove-duplicatesのみ)"
|
|
96
|
+
examples: "Examples:"
|
|
97
|
+
ex_setup: |2
|
|
98
|
+
tidyify setup # Setup current directory
|
|
99
|
+
tidyify setup ~/Downloads # Setup specific directory
|
|
100
|
+
ex_run_current: " tidyify run # Organize current directory"
|
|
101
|
+
ex_run_dry: " tidyify run ~/Downloads --dry-run # Dry-run simulation"
|
|
102
|
+
ex_run_exec: " tidyify run ~/Downloads --recursive # Execute with recursive"
|
|
103
|
+
ex_organize_date: " tidyify organize-by-date ~/Downloads --pattern=year-month"
|
|
104
|
+
ex_find_dup: " tidyify find-duplicates ~/Downloads --recursive"
|
|
105
|
+
ex_remove_dup: " tidyify remove-duplicates ~/Downloads --recursive"
|
|
106
|
+
ex_remove_no_confirm: " tidyify remove-duplicates ~/Downloads --no-confirm"
|
|
107
|
+
|
|
108
|
+
post_install:
|
|
109
|
+
created_default_en: "✓ デフォルト設定ファイル(英語)を作成しました: %{path}"
|
|
110
|
+
created_default_ja: "✓ デフォルト設定ファイル(日本語)を作成しました: %{path}"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
|
|
3
|
+
module TidyFileOrganizer
|
|
4
|
+
class Organizer
|
|
5
|
+
include FileHelper
|
|
6
|
+
|
|
7
|
+
attr_reader :target_dir
|
|
8
|
+
|
|
9
|
+
def initialize(target_dir)
|
|
10
|
+
@target_dir = File.expand_path(target_dir)
|
|
11
|
+
@config_manager = Config.new(@target_dir)
|
|
12
|
+
@file_mover = FileMover.new(@target_dir)
|
|
13
|
+
@organized_dirs = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def setup
|
|
17
|
+
SetupPrompt.new(@config_manager).run(@target_dir)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run(dry_run: false, recursive: false)
|
|
21
|
+
config = @config_manager.load
|
|
22
|
+
return handle_missing_config unless config
|
|
23
|
+
|
|
24
|
+
print_header(dry_run, recursive)
|
|
25
|
+
|
|
26
|
+
@organized_dirs = extract_organized_dirs(config)
|
|
27
|
+
|
|
28
|
+
files = collect_files(@target_dir, recursive: recursive, exclude_dirs: @organized_dirs)
|
|
29
|
+
return handle_empty_files if files.empty?
|
|
30
|
+
|
|
31
|
+
organize_files(files, config, dry_run)
|
|
32
|
+
cleanup_empty_directories if recursive && !dry_run
|
|
33
|
+
|
|
34
|
+
print_completion_message
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def handle_missing_config
|
|
40
|
+
puts I18n.t('organizer.no_config')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def print_header(dry_run, recursive)
|
|
44
|
+
mode_label = dry_run ? I18n.t('organizer.dry_run_mode') : ''
|
|
45
|
+
recursive_label = recursive ? I18n.t('organizer.recursive_mode') : ''
|
|
46
|
+
puts I18n.t('organizer.starting', dir: @target_dir, mode: "#{mode_label} #{recursive_label}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def extract_organized_dirs(config)
|
|
50
|
+
(config[:extensions].keys + config[:keywords].keys).uniq
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_empty_files
|
|
54
|
+
puts I18n.t('organizer.no_files')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def print_completion_message
|
|
58
|
+
puts "\n#{I18n.t('organizer.completed')}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def organize_files(files, config, dry_run)
|
|
62
|
+
files.each do |file_path|
|
|
63
|
+
destination_dir = determine_destination(file_path, config)
|
|
64
|
+
next unless destination_dir
|
|
65
|
+
|
|
66
|
+
@file_mover.move_file(file_path, destination_dir, dry_run: dry_run)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def determine_destination(file_path, config)
|
|
71
|
+
filename = File.basename(file_path)
|
|
72
|
+
extension = extract_extension(file_path)
|
|
73
|
+
|
|
74
|
+
find_by_keyword(filename, config[:keywords]) ||
|
|
75
|
+
find_by_extension(extension, config[:extensions])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def extract_extension(file_path)
|
|
79
|
+
File.extname(file_path).delete('.').downcase
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def find_by_keyword(filename, keywords_config)
|
|
83
|
+
keywords_config.each do |dir, keywords|
|
|
84
|
+
return dir if keywords.any? { |kw| filename.include?(kw) }
|
|
85
|
+
end
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def find_by_extension(extension, extensions_config)
|
|
90
|
+
extensions_config.each do |dir, extensions|
|
|
91
|
+
return dir if extensions.include?(extension)
|
|
92
|
+
end
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def cleanup_empty_directories
|
|
97
|
+
Dir.glob(File.join(@target_dir, '**/*')).reverse_each do |path|
|
|
98
|
+
next unless should_cleanup?(path)
|
|
99
|
+
|
|
100
|
+
remove_empty_directory(path)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def should_cleanup?(path)
|
|
105
|
+
File.directory?(path) &&
|
|
106
|
+
path != @target_dir &&
|
|
107
|
+
!@organized_dirs.include?(File.basename(path)) &&
|
|
108
|
+
Dir.empty?(path)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def remove_empty_directory(path)
|
|
112
|
+
Dir.rmdir(path)
|
|
113
|
+
relative = relative_path(path, @target_dir)
|
|
114
|
+
puts I18n.t('organizer.cleaned_up', dir: relative)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require_relative 'i18n'
|
|
3
|
+
|
|
4
|
+
module TidyFileOrganizer
|
|
5
|
+
class PostInstall
|
|
6
|
+
CONFIG_DIR = File.expand_path('~/.config/tidy-file-organizer').freeze
|
|
7
|
+
DEFAULT_CONFIG_PATH = File.join(CONFIG_DIR, 'default.yml').freeze
|
|
8
|
+
DEFAULT_CONFIG_JA_PATH = File.join(CONFIG_DIR, 'default.ja.yml').freeze
|
|
9
|
+
|
|
10
|
+
def self.run
|
|
11
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
|
12
|
+
|
|
13
|
+
# 英語版(デフォルト)
|
|
14
|
+
source = File.expand_path('../../config/default.yml', __dir__)
|
|
15
|
+
unless File.exist?(DEFAULT_CONFIG_PATH)
|
|
16
|
+
FileUtils.cp(source, DEFAULT_CONFIG_PATH)
|
|
17
|
+
puts I18n.t('post_install.created_default_en', path: DEFAULT_CONFIG_PATH)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 日本語版
|
|
21
|
+
source_ja = File.expand_path('../../config/default.ja.yml', __dir__)
|
|
22
|
+
unless File.exist?(DEFAULT_CONFIG_JA_PATH)
|
|
23
|
+
FileUtils.cp(source_ja, DEFAULT_CONFIG_JA_PATH)
|
|
24
|
+
puts I18n.t('post_install.created_default_ja', path: DEFAULT_CONFIG_JA_PATH)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Auto-execute on gem install
|
|
31
|
+
TidyFileOrganizer::PostInstall.run if __FILE__ == $PROGRAM_NAME
|