file_tree_visualizer 0.0.3 → 0.2.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 +4 -4
- data/Gemfile +2 -0
- data/README.md +111 -23
- data/Rakefile +8 -0
- data/file_tree_visualizer.gemspec +22 -20
- data/lib/file_tree_visualizer/cli.rb +188 -34
- data/lib/file_tree_visualizer/i18n.rb +205 -0
- data/lib/file_tree_visualizer/report_builder.rb +50 -20
- data/lib/file_tree_visualizer/scanner.rb +30 -4
- data/lib/file_tree_visualizer/version.rb +1 -1
- data/lib/file_tree_visualizer.rb +5 -4
- metadata +23 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b43d301f84619b7a620a0cbcef1d135d4906073efaecf03b737b43572ebd6d89
|
|
4
|
+
data.tar.gz: 65de782e8d5094d981f0bedd23e6307d1b6cd4eedebd6b8bbb412a9b303e330e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6a58da21e94d26fd62fa5a05bc5d0946a16525c187f021e7184b010515051f346b2c8786067879c278c34ca3abfbe61dc416f1a2bd170201654c71d477bffd0
|
|
7
|
+
data.tar.gz: ba3cb73876856ee460c66f1b34a61f1965cb6deda2021f52dba5904a8500b995e8cd4c28fd91bb1f388462a88c2d62e381024e94d1430dfaf591555344c674a8
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,44 +1,132 @@
|
|
|
1
1
|
# file_tree_visualizer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/JohnBidwellB/file-tree-visualizer/actions/workflows/release.yml)
|
|
4
|
+
[](https://rubygems.org/gems/file_tree_visualizer)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Ruby gem to scan file trees from the terminal and generate a fully offline interactive HTML report with the items that consume the most disk space.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- Calcula tamaño acumulado por directorio y tamaño por archivo.
|
|
9
|
-
- Genera un reporte HTML con:
|
|
10
|
-
- Árbol de archivos expandible/colapsable.
|
|
11
|
-
- Filtros por texto y tamaño mínimo.
|
|
12
|
-
- Ranking de archivos más pesados.
|
|
13
|
-
- Sin dependencias externas (funciona sin internet).
|
|
8
|
+
## What it does
|
|
14
9
|
|
|
15
|
-
|
|
10
|
+
- Recursively scans a directory.
|
|
11
|
+
- Computes cumulative size per directory and size per file.
|
|
12
|
+
- Generates an HTML report with:
|
|
13
|
+
- Expand/collapse file tree.
|
|
14
|
+
- Text and minimum-size filters.
|
|
15
|
+
- Ranking of the largest files.
|
|
16
|
+
- No external dependencies (works offline).
|
|
17
|
+
|
|
18
|
+
## Quick usage (without installing the gem)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
ruby bin/file-tree-visualizer /path/to/scan -o report.html
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Install from RubyGems
|
|
25
|
+
|
|
26
|
+
Gem page: https://rubygems.org/gems/file_tree_visualizer
|
|
16
27
|
|
|
17
28
|
```bash
|
|
18
|
-
|
|
29
|
+
gem install file_tree_visualizer
|
|
30
|
+
file-tree-visualizer /path/to/scan -o report.html
|
|
19
31
|
```
|
|
20
32
|
|
|
21
|
-
|
|
33
|
+
Install a specific version:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
gem install file_tree_visualizer -v 0.2.0
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Local development install
|
|
22
40
|
|
|
23
41
|
```bash
|
|
24
42
|
bundle install
|
|
25
43
|
gem build file_tree_visualizer.gemspec
|
|
26
|
-
gem install ./file_tree_visualizer-0.0.
|
|
27
|
-
file-tree-visualizer /
|
|
44
|
+
gem install ./file_tree_visualizer-0.2.0.gem
|
|
45
|
+
file-tree-visualizer /path/to/scan -o report.html
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Options
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
file-tree-visualizer [path] [options]
|
|
52
|
+
|
|
53
|
+
If no `path` is provided, the current directory is scanned by default.
|
|
54
|
+
|
|
55
|
+
During scanning, an interactive terminal shows a progress bar with processed files/directories vs discovered totals.
|
|
56
|
+
|
|
57
|
+
-o, --output FILE Output HTML file (default: file-tree-report.html)
|
|
58
|
+
-t, --top N Number of largest files to keep in ranking (default: 200)
|
|
59
|
+
--follow-symlinks Follow symbolic links
|
|
60
|
+
--no-progress Disable progress bar
|
|
61
|
+
--exclude PATTERNS Exclude patterns (comma-separated or repeated)
|
|
62
|
+
--summary Print summary to terminal
|
|
63
|
+
--json FILE Export full result as JSON
|
|
64
|
+
--csv FILE Export top files as CSV
|
|
65
|
+
--lang LANG Language for CLI and report (en|es)
|
|
66
|
+
-h, --help Show help
|
|
67
|
+
|
|
68
|
+
Language resolution priority:
|
|
69
|
+
- `--lang`
|
|
70
|
+
- `LANG` environment variable (for example `es_CL.UTF-8`)
|
|
71
|
+
- fallback to English (`en`)
|
|
28
72
|
```
|
|
29
73
|
|
|
30
|
-
##
|
|
74
|
+
## Common workflows
|
|
75
|
+
|
|
76
|
+
Summary only (no HTML file generated unless `--output` is explicitly provided):
|
|
31
77
|
|
|
32
78
|
```bash
|
|
33
|
-
file-tree-visualizer
|
|
79
|
+
file-tree-visualizer --summary
|
|
80
|
+
```
|
|
34
81
|
|
|
35
|
-
|
|
82
|
+
Summary + HTML report:
|
|
36
83
|
|
|
37
|
-
|
|
84
|
+
```bash
|
|
85
|
+
file-tree-visualizer . --summary --output report.html
|
|
86
|
+
```
|
|
38
87
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
88
|
+
Exclude directories and files by pattern:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
file-tree-visualizer . --exclude "node_modules,.git,*.log" --exclude "tmp/**"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Export JSON and CSV in one run:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
file-tree-visualizer . --summary --json scan.json --csv top-files.csv
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Contributing
|
|
101
|
+
|
|
102
|
+
1. Create a feature branch from `main`.
|
|
103
|
+
2. Implement your change and run tests locally.
|
|
104
|
+
3. Push your branch and open a Pull Request.
|
|
105
|
+
4. Wait for CI to pass and at least one approval.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git checkout -b feat/short-description
|
|
109
|
+
bundle exec rake test
|
|
110
|
+
git push origin feat/short-description
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Branch policy:
|
|
114
|
+
- For collaborators, `main` is protected and requires PR + CI + approval.
|
|
115
|
+
- Repository admins can bypass these rules when needed.
|
|
116
|
+
|
|
117
|
+
## Automatic release to RubyGems
|
|
118
|
+
|
|
119
|
+
- This repository publishes the gem automatically when pushing a tag with format `v*` (for example `v0.2.0`).
|
|
120
|
+
- Before using it, configure the `RUBYGEMS_API_KEY` secret in GitHub Actions.
|
|
121
|
+
- You can also trigger the workflow manually from GitHub Actions (`workflow_dispatch`).
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
git tag v0.2.0
|
|
125
|
+
git push origin v0.2.0
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Reusable release notes template:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
gh release create v0.2.0 --title "v0.2.0" --notes-file .github/release-template.md
|
|
44
132
|
```
|
data/Rakefile
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'lib/file_tree_visualizer/version'
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name =
|
|
6
|
+
spec.name = 'file_tree_visualizer'
|
|
7
7
|
spec.version = FileTreeVisualizer::VERSION
|
|
8
|
-
spec.authors = [
|
|
9
|
-
spec.email = [
|
|
8
|
+
spec.authors = ['John Bidwell']
|
|
9
|
+
spec.email = ['johnbidwellb@gmail.com']
|
|
10
10
|
|
|
11
|
-
spec.summary =
|
|
12
|
-
spec.description =
|
|
13
|
-
spec.homepage =
|
|
14
|
-
spec.license =
|
|
15
|
-
spec.required_ruby_version =
|
|
11
|
+
spec.summary = 'CLI Ruby tool to analyze file sizes and generate an interactive HTML report.'
|
|
12
|
+
spec.description = 'Scans a directory recursively, computes file/directory sizes, and generates an HTML report rendered with React.'
|
|
13
|
+
spec.homepage = 'https://github.com/JohnBidwellB/file-tree-visualizer'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 2.7'
|
|
16
16
|
|
|
17
|
-
spec.metadata[
|
|
18
|
-
spec.metadata[
|
|
17
|
+
spec.metadata['source_code_uri'] = "#{spec.homepage}/tree/main"
|
|
18
|
+
spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues"
|
|
19
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
19
20
|
|
|
20
21
|
spec.files = Dir.chdir(__dir__) do
|
|
21
22
|
Dir[
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
'README.md',
|
|
24
|
+
'Gemfile',
|
|
25
|
+
'Rakefile',
|
|
26
|
+
'bin/*',
|
|
27
|
+
'lib/**/*.rb',
|
|
28
|
+
'file_tree_visualizer.gemspec'
|
|
28
29
|
]
|
|
29
30
|
end
|
|
30
|
-
spec.bindir =
|
|
31
|
-
spec.executables = [
|
|
32
|
-
spec.require_paths = [
|
|
31
|
+
spec.bindir = 'bin'
|
|
32
|
+
spec.executables = ['file-tree-visualizer']
|
|
33
|
+
spec.require_paths = ['lib']
|
|
34
|
+
spec.add_dependency 'csv', '~> 3.3'
|
|
33
35
|
end
|
|
@@ -1,42 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'csv'
|
|
5
|
+
require 'json'
|
|
4
6
|
|
|
5
7
|
module FileTreeVisualizer
|
|
6
8
|
class CLI
|
|
7
|
-
DEFAULT_OUTPUT =
|
|
9
|
+
DEFAULT_OUTPUT = 'file-tree-report.html'
|
|
10
|
+
DEFAULT_LOCALE = 'en'
|
|
8
11
|
|
|
9
12
|
def initialize(argv)
|
|
10
13
|
@argv = argv
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def run
|
|
14
|
-
options, parser = parse_options(@argv.dup)
|
|
17
|
+
options, parser, locale = parse_options(@argv.dup)
|
|
15
18
|
if options[:help]
|
|
16
19
|
puts parser
|
|
17
20
|
return 0
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
expanded_path = File.expand_path(options[:path])
|
|
24
|
+
unless File.directory?(expanded_path)
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
t(locale, 'cli.errors.invalid_directory', path: expanded_path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
scanner = Scanner.new(
|
|
30
|
+
top_limit: options[:top],
|
|
31
|
+
follow_symlinks: options[:follow_symlinks],
|
|
32
|
+
excludes: options[:exclude_patterns]
|
|
33
|
+
)
|
|
34
|
+
progress = ProgressRenderer.new(enabled: options[:progress], locale: locale)
|
|
22
35
|
callback = options[:progress] ? progress.method(:render) : nil
|
|
23
|
-
result = scanner.scan(
|
|
36
|
+
result = scanner.scan(expanded_path, progress: callback)
|
|
24
37
|
progress.finish
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
puts
|
|
31
|
-
puts
|
|
32
|
-
puts
|
|
38
|
+
generated_at = Time.now.utc
|
|
39
|
+
output_path = maybe_write_report(options, expanded_path, result, locale, generated_at)
|
|
40
|
+
json_path = write_json_export(options[:json_output], expanded_path, result, locale, generated_at)
|
|
41
|
+
csv_path = write_csv_export(options[:csv_output], result)
|
|
42
|
+
print_summary(expanded_path, result, locale) if options[:summary]
|
|
43
|
+
puts t(locale, 'cli.messages.report_generated', path: output_path) if output_path
|
|
44
|
+
puts t(locale, 'cli.messages.json_generated', path: json_path) if json_path
|
|
45
|
+
puts t(locale, 'cli.messages.csv_generated', path: csv_path) if csv_path
|
|
46
|
+
unless options[:summary]
|
|
47
|
+
puts t(locale, 'cli.messages.scanned',
|
|
48
|
+
directories: result.stats[:directories],
|
|
49
|
+
files: result.stats[:files],
|
|
50
|
+
skipped: result.stats[:skipped])
|
|
51
|
+
end
|
|
52
|
+
puts t(locale, 'cli.messages.total_size', size: format_bytes(result.stats[:bytes])) unless options[:summary]
|
|
33
53
|
0
|
|
34
54
|
rescue OptionParser::ParseError, ArgumentError => e
|
|
55
|
+
locale = error_locale
|
|
35
56
|
warn e.message
|
|
36
|
-
warn
|
|
57
|
+
warn t(locale, 'cli.errors.use_help')
|
|
37
58
|
1
|
|
38
59
|
rescue StandardError => e
|
|
39
|
-
|
|
60
|
+
locale = error_locale
|
|
61
|
+
warn t(locale, 'cli.errors.unexpected', message: e.message)
|
|
40
62
|
1
|
|
41
63
|
end
|
|
42
64
|
|
|
@@ -45,9 +67,10 @@ module FileTreeVisualizer
|
|
|
45
67
|
class ProgressRenderer
|
|
46
68
|
BAR_WIDTH = 28
|
|
47
69
|
|
|
48
|
-
def initialize(io: $stdout, enabled: true)
|
|
70
|
+
def initialize(io: $stdout, enabled: true, locale: DEFAULT_LOCALE)
|
|
49
71
|
@io = io
|
|
50
72
|
@enabled = enabled && io.tty?
|
|
73
|
+
@locale = locale
|
|
51
74
|
@last_draw_at = nil
|
|
52
75
|
@has_rendered = false
|
|
53
76
|
@spinner_index = 0
|
|
@@ -82,7 +105,7 @@ module FileTreeVisualizer
|
|
|
82
105
|
def render_counting(data)
|
|
83
106
|
spinner = %w[| / - \\][@spinner_index % 4]
|
|
84
107
|
@spinner_index += 1
|
|
85
|
-
@io.print(format("\
|
|
108
|
+
@io.print(format("\r#{I18n.t(@locale, 'cli.progress.preparing')}",
|
|
86
109
|
spinner: spinner,
|
|
87
110
|
files: data.fetch(:discovered_files, 0),
|
|
88
111
|
dirs: data.fetch(:discovered_directories, 0)))
|
|
@@ -102,9 +125,9 @@ module FileTreeVisualizer
|
|
|
102
125
|
empty = BAR_WIDTH - filled
|
|
103
126
|
percent = progress_ratio * 100
|
|
104
127
|
|
|
105
|
-
@io.print(format("\
|
|
106
|
-
bar: (
|
|
107
|
-
percent: percent,
|
|
128
|
+
@io.print(format("\r#{I18n.t(@locale, 'cli.progress.scanning')}",
|
|
129
|
+
bar: ('#' * filled) + ('-' * empty),
|
|
130
|
+
percent: format('%6.2f', percent),
|
|
108
131
|
pf: processed_files,
|
|
109
132
|
tf: total_files,
|
|
110
133
|
pd: processed_directories,
|
|
@@ -113,46 +136,110 @@ module FileTreeVisualizer
|
|
|
113
136
|
end
|
|
114
137
|
|
|
115
138
|
def parse_options(argv)
|
|
139
|
+
locale = I18n.resolve_locale(cli_value: extract_lang_argument(argv), env_lang: ENV['LANG'])
|
|
116
140
|
options = {
|
|
117
141
|
path: Dir.pwd,
|
|
118
142
|
output: DEFAULT_OUTPUT,
|
|
119
143
|
top: Scanner::DEFAULT_TOP_LIMIT,
|
|
120
144
|
follow_symlinks: false,
|
|
121
145
|
progress: true,
|
|
146
|
+
summary: false,
|
|
147
|
+
output_given: false,
|
|
148
|
+
json_output: nil,
|
|
149
|
+
csv_output: nil,
|
|
150
|
+
exclude_patterns: [],
|
|
151
|
+
lang: nil,
|
|
122
152
|
help: false
|
|
123
153
|
}
|
|
124
154
|
|
|
125
|
-
parser =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
155
|
+
parser = build_parser(locale, options)
|
|
156
|
+
remaining = parser.parse(argv)
|
|
157
|
+
|
|
158
|
+
if options[:lang] && !I18n.supported_locale?(options[:lang])
|
|
159
|
+
raise ArgumentError, t(locale,
|
|
160
|
+
'cli.errors.invalid_lang',
|
|
161
|
+
lang: options[:lang],
|
|
162
|
+
supported: I18n::SUPPORTED_LOCALES.join(', '))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
locale = I18n.resolve_locale(cli_value: options[:lang], env_lang: ENV['LANG'])
|
|
166
|
+
parser = build_parser(locale, options)
|
|
129
167
|
|
|
130
|
-
|
|
168
|
+
options[:path] = File.expand_path(remaining.first || options[:path])
|
|
169
|
+
raise ArgumentError, t(locale, 'cli.errors.invalid_top') unless options[:top].positive?
|
|
170
|
+
|
|
171
|
+
[options, parser, locale]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def build_parser(locale, options)
|
|
175
|
+
OptionParser.new do |opts|
|
|
176
|
+
opts.banner = t(locale, 'cli.banner')
|
|
177
|
+
opts.separator ''
|
|
178
|
+
opts.separator t(locale, 'cli.options_header')
|
|
179
|
+
|
|
180
|
+
opts.on('-o', '--output FILE', t(locale, 'cli.output_option', default: DEFAULT_OUTPUT)) do |value|
|
|
131
181
|
options[:output] = value
|
|
182
|
+
options[:output_given] = true
|
|
132
183
|
end
|
|
133
184
|
|
|
134
|
-
opts.on(
|
|
185
|
+
opts.on('-t', '--top N', Integer, t(locale, 'cli.top_option', default: Scanner::DEFAULT_TOP_LIMIT)) do |value|
|
|
135
186
|
options[:top] = value
|
|
136
187
|
end
|
|
137
188
|
|
|
138
|
-
opts.on(
|
|
189
|
+
opts.on('--follow-symlinks', t(locale, 'cli.follow_symlinks_option')) do
|
|
139
190
|
options[:follow_symlinks] = true
|
|
140
191
|
end
|
|
141
192
|
|
|
142
|
-
opts.on(
|
|
193
|
+
opts.on('--no-progress', t(locale, 'cli.no_progress_option')) do
|
|
143
194
|
options[:progress] = false
|
|
144
195
|
end
|
|
145
196
|
|
|
146
|
-
opts.on(
|
|
197
|
+
opts.on('--exclude PATTERNS', t(locale, 'cli.exclude_option')) do |value|
|
|
198
|
+
options[:exclude_patterns].concat(split_patterns(value))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
opts.on('--summary', t(locale, 'cli.summary_option')) do
|
|
202
|
+
options[:summary] = true
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
opts.on('--json FILE', t(locale, 'cli.json_option')) do |value|
|
|
206
|
+
options[:json_output] = value
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
opts.on('--csv FILE', t(locale, 'cli.csv_option')) do |value|
|
|
210
|
+
options[:csv_output] = value
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
opts.on('--lang LANG', t(locale, 'cli.lang_option')) do |value|
|
|
214
|
+
options[:lang] = value
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
opts.on('-h', '--help', t(locale, 'cli.help_option')) do
|
|
147
218
|
options[:help] = true
|
|
148
219
|
end
|
|
149
220
|
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def extract_lang_argument(argv)
|
|
224
|
+
argv.each_with_index do |arg, index|
|
|
225
|
+
return arg.split('=', 2).last if arg.start_with?('--lang=')
|
|
226
|
+
next unless arg == '--lang'
|
|
227
|
+
|
|
228
|
+
return argv[index + 1]
|
|
229
|
+
end
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
150
232
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
233
|
+
def split_patterns(value)
|
|
234
|
+
value.to_s.split(',').map(&:strip).reject(&:empty?)
|
|
235
|
+
end
|
|
154
236
|
|
|
155
|
-
|
|
237
|
+
def t(locale, key, **vars)
|
|
238
|
+
I18n.t(locale, key, **vars)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def error_locale
|
|
242
|
+
I18n.resolve_locale(cli_value: extract_lang_argument(@argv), env_lang: ENV['LANG'])
|
|
156
243
|
end
|
|
157
244
|
|
|
158
245
|
def format_bytes(bytes)
|
|
@@ -170,5 +257,72 @@ module FileTreeVisualizer
|
|
|
170
257
|
precision = value < 10 ? 2 : 1
|
|
171
258
|
format("%.#{precision}f %s", value, units[unit_index])
|
|
172
259
|
end
|
|
260
|
+
|
|
261
|
+
def maybe_write_report(options, source_path, result, locale, generated_at)
|
|
262
|
+
should_write = !options[:summary] || options[:output_given]
|
|
263
|
+
return nil unless should_write
|
|
264
|
+
|
|
265
|
+
report = ReportBuilder.new(
|
|
266
|
+
result: result,
|
|
267
|
+
source_path: source_path,
|
|
268
|
+
locale: locale,
|
|
269
|
+
generated_at: generated_at
|
|
270
|
+
).build
|
|
271
|
+
output_path = File.expand_path(options[:output])
|
|
272
|
+
File.write(output_path, report)
|
|
273
|
+
output_path
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def write_json_export(target_path, source_path, result, locale, generated_at)
|
|
277
|
+
return nil unless target_path
|
|
278
|
+
|
|
279
|
+
output_path = File.expand_path(target_path)
|
|
280
|
+
payload = {
|
|
281
|
+
source_path: source_path,
|
|
282
|
+
generated_at: generated_at.iso8601,
|
|
283
|
+
locale: locale,
|
|
284
|
+
version: FileTreeVisualizer::VERSION,
|
|
285
|
+
stats: result.stats,
|
|
286
|
+
root: result.root,
|
|
287
|
+
top_files: result.top_files
|
|
288
|
+
}
|
|
289
|
+
File.write(output_path, JSON.pretty_generate(payload))
|
|
290
|
+
output_path
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def write_csv_export(target_path, result)
|
|
294
|
+
return nil unless target_path
|
|
295
|
+
|
|
296
|
+
output_path = File.expand_path(target_path)
|
|
297
|
+
CSV.open(output_path, 'w') do |csv|
|
|
298
|
+
csv << %w[rank path name size_bytes size_human]
|
|
299
|
+
result.top_files.each_with_index do |item, index|
|
|
300
|
+
csv << [index + 1, item[:path], item[:name], item[:size], format_bytes(item[:size])]
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
output_path
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def print_summary(source_path, result, locale)
|
|
307
|
+
puts t(locale, 'cli.summary.heading')
|
|
308
|
+
puts t(locale, 'cli.summary.source', path: source_path)
|
|
309
|
+
puts t(locale, 'cli.summary.total_size', size: format_bytes(result.stats[:bytes]))
|
|
310
|
+
puts t(locale, 'cli.summary.files', count: result.stats[:files])
|
|
311
|
+
puts t(locale, 'cli.summary.directories', count: result.stats[:directories])
|
|
312
|
+
puts t(locale, 'cli.summary.skipped', count: result.stats[:skipped])
|
|
313
|
+
puts t(locale, 'cli.summary.top_files')
|
|
314
|
+
|
|
315
|
+
if result.top_files.empty?
|
|
316
|
+
puts " #{t(locale, 'cli.summary.none')}"
|
|
317
|
+
return
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
result.top_files.each_with_index do |item, index|
|
|
321
|
+
puts format(' %<rank>d. %<size>s %<path>s',
|
|
322
|
+
rank: index + 1,
|
|
323
|
+
size: format_bytes(item[:size]),
|
|
324
|
+
path: item[:path])
|
|
325
|
+
end
|
|
326
|
+
end
|
|
173
327
|
end
|
|
174
328
|
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FileTreeVisualizer
|
|
4
|
+
module I18n
|
|
5
|
+
SUPPORTED_LOCALES = %w[en es].freeze
|
|
6
|
+
|
|
7
|
+
TRANSLATIONS = {
|
|
8
|
+
'en' => {
|
|
9
|
+
'cli' => {
|
|
10
|
+
'banner' => 'Usage: file-tree-visualizer [path] [options]',
|
|
11
|
+
'options_header' => 'Options:',
|
|
12
|
+
'output_option' => 'Output HTML file (default: %<default>s)',
|
|
13
|
+
'top_option' => 'Number of largest files in ranking (default: %<default>s)',
|
|
14
|
+
'follow_symlinks_option' => 'Follow symbolic links during scan',
|
|
15
|
+
'no_progress_option' => 'Disable progress bar',
|
|
16
|
+
'exclude_option' => 'Exclude patterns (comma-separated or repeated)',
|
|
17
|
+
'summary_option' => 'Print terminal summary',
|
|
18
|
+
'json_option' => 'Export scan result as JSON',
|
|
19
|
+
'csv_option' => 'Export top files as CSV',
|
|
20
|
+
'lang_option' => 'Language for CLI and report (en|es)',
|
|
21
|
+
'help_option' => 'Show this help',
|
|
22
|
+
'messages' => {
|
|
23
|
+
'report_generated' => 'Report generated at: %<path>s',
|
|
24
|
+
'json_generated' => 'JSON exported at: %<path>s',
|
|
25
|
+
'csv_generated' => 'CSV exported at: %<path>s',
|
|
26
|
+
'scanned' => 'Scanned: %<directories>s directories, %<files>s files, %<skipped>s skipped.',
|
|
27
|
+
'total_size' => 'Total size: %<size>s'
|
|
28
|
+
},
|
|
29
|
+
'errors' => {
|
|
30
|
+
'invalid_top' => '--top must be greater than 0',
|
|
31
|
+
'invalid_lang' => "Unsupported language '%<lang>s'. Supported values: %<supported>s",
|
|
32
|
+
'invalid_directory' => 'Path is not a directory: %<path>s',
|
|
33
|
+
'use_help' => 'Use --help to see available options.',
|
|
34
|
+
'unexpected' => 'Unexpected error: %<message>s'
|
|
35
|
+
},
|
|
36
|
+
'progress' => {
|
|
37
|
+
'preparing' => 'Preparing scan %<spinner>s Files:%<files>s Dirs:%<dirs>s',
|
|
38
|
+
'scanning' => 'Scanning [%<bar>s] %<percent>s%% Files:%<pf>s/%<tf>s Dirs:%<pd>s/%<td>s'
|
|
39
|
+
},
|
|
40
|
+
'summary' => {
|
|
41
|
+
'heading' => 'Summary',
|
|
42
|
+
'source' => 'Source: %<path>s',
|
|
43
|
+
'total_size' => 'Total size: %<size>s',
|
|
44
|
+
'files' => 'Files: %<count>s',
|
|
45
|
+
'directories' => 'Directories: %<count>s',
|
|
46
|
+
'skipped' => 'Skipped entries: %<count>s',
|
|
47
|
+
'top_files' => 'Top files:',
|
|
48
|
+
'none' => '(none)'
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
'report' => {
|
|
52
|
+
'page_title' => 'File Report',
|
|
53
|
+
'header_title' => 'Disk usage report',
|
|
54
|
+
'meta_source_prefix' => 'Source: ',
|
|
55
|
+
'meta_generated_prefix' => 'Generated: ',
|
|
56
|
+
'stats' => {
|
|
57
|
+
'total_size' => 'Total size',
|
|
58
|
+
'files' => 'Files',
|
|
59
|
+
'directories' => 'Directories',
|
|
60
|
+
'skipped' => 'Skipped entries'
|
|
61
|
+
},
|
|
62
|
+
'filters' => {
|
|
63
|
+
'search_label' => 'Search by name/path',
|
|
64
|
+
'search_placeholder' => 'eg: node_modules, .mp4, backup',
|
|
65
|
+
'min_size_label' => 'Minimum size (MB)',
|
|
66
|
+
'min_size_hint' => 'Filters tree nodes and ranking by minimum size.'
|
|
67
|
+
},
|
|
68
|
+
'panels' => {
|
|
69
|
+
'tree' => 'File tree',
|
|
70
|
+
'top_files' => 'Top largest files'
|
|
71
|
+
},
|
|
72
|
+
'table' => {
|
|
73
|
+
'index' => '#',
|
|
74
|
+
'size' => 'Size',
|
|
75
|
+
'path' => 'Path'
|
|
76
|
+
},
|
|
77
|
+
'empty' => {
|
|
78
|
+
'tree' => 'No results with current filters.',
|
|
79
|
+
'top_files' => 'No files to show with current filters.'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
'es' => {
|
|
84
|
+
'cli' => {
|
|
85
|
+
'banner' => 'Uso: file-tree-visualizer [ruta] [opciones]',
|
|
86
|
+
'options_header' => 'Opciones:',
|
|
87
|
+
'output_option' => 'Archivo HTML de salida (default: %<default>s)',
|
|
88
|
+
'top_option' => 'Cantidad de archivos pesados en ranking (default: %<default>s)',
|
|
89
|
+
'follow_symlinks_option' => 'Sigue enlaces simbólicos durante el escaneo',
|
|
90
|
+
'no_progress_option' => 'Desactiva la barra de progreso',
|
|
91
|
+
'exclude_option' => 'Excluye patrones (separados por coma o repetidos)',
|
|
92
|
+
'summary_option' => 'Imprime resumen en terminal',
|
|
93
|
+
'json_option' => 'Exporta resultado del escaneo como JSON',
|
|
94
|
+
'csv_option' => 'Exporta top de archivos como CSV',
|
|
95
|
+
'lang_option' => 'Idioma para CLI y reporte (en|es)',
|
|
96
|
+
'help_option' => 'Muestra esta ayuda',
|
|
97
|
+
'messages' => {
|
|
98
|
+
'report_generated' => 'Reporte generado en: %<path>s',
|
|
99
|
+
'json_generated' => 'JSON exportado en: %<path>s',
|
|
100
|
+
'csv_generated' => 'CSV exportado en: %<path>s',
|
|
101
|
+
'scanned' => 'Escaneado: %<directories>s directorios, %<files>s archivos, %<skipped>s omitidos.',
|
|
102
|
+
'total_size' => 'Tamano total: %<size>s'
|
|
103
|
+
},
|
|
104
|
+
'errors' => {
|
|
105
|
+
'invalid_top' => '--top debe ser mayor que 0',
|
|
106
|
+
'invalid_lang' => "Idioma no soportado '%<lang>s'. Valores permitidos: %<supported>s",
|
|
107
|
+
'invalid_directory' => 'La ruta no es un directorio: %<path>s',
|
|
108
|
+
'use_help' => 'Usa --help para ver las opciones disponibles.',
|
|
109
|
+
'unexpected' => 'Error inesperado: %<message>s'
|
|
110
|
+
},
|
|
111
|
+
'progress' => {
|
|
112
|
+
'preparing' => 'Preparando escaneo %<spinner>s Arch:%<files>s Dir:%<dirs>s',
|
|
113
|
+
'scanning' => 'Escaneando [%<bar>s] %<percent>s%% Arch:%<pf>s/%<tf>s Dir:%<pd>s/%<td>s'
|
|
114
|
+
},
|
|
115
|
+
'summary' => {
|
|
116
|
+
'heading' => 'Resumen',
|
|
117
|
+
'source' => 'Origen: %<path>s',
|
|
118
|
+
'total_size' => 'Tamano total: %<size>s',
|
|
119
|
+
'files' => 'Archivos: %<count>s',
|
|
120
|
+
'directories' => 'Directorios: %<count>s',
|
|
121
|
+
'skipped' => 'Entradas omitidas: %<count>s',
|
|
122
|
+
'top_files' => 'Top archivos:',
|
|
123
|
+
'none' => '(ninguno)'
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
'report' => {
|
|
127
|
+
'page_title' => 'Reporte de archivos',
|
|
128
|
+
'header_title' => 'Reporte de espacio en disco',
|
|
129
|
+
'meta_source_prefix' => 'Origen: ',
|
|
130
|
+
'meta_generated_prefix' => 'Generado: ',
|
|
131
|
+
'stats' => {
|
|
132
|
+
'total_size' => 'Tamano total',
|
|
133
|
+
'files' => 'Archivos',
|
|
134
|
+
'directories' => 'Directorios',
|
|
135
|
+
'skipped' => 'Entradas omitidas'
|
|
136
|
+
},
|
|
137
|
+
'filters' => {
|
|
138
|
+
'search_label' => 'Buscar por nombre/ruta',
|
|
139
|
+
'search_placeholder' => 'ej: node_modules, .mp4, backup',
|
|
140
|
+
'min_size_label' => 'Tamano minimo (MB)',
|
|
141
|
+
'min_size_hint' => 'Filtra nodos y ranking por tamano minimo.'
|
|
142
|
+
},
|
|
143
|
+
'panels' => {
|
|
144
|
+
'tree' => 'Arbol de archivos',
|
|
145
|
+
'top_files' => 'Top archivos mas pesados'
|
|
146
|
+
},
|
|
147
|
+
'table' => {
|
|
148
|
+
'index' => '#',
|
|
149
|
+
'size' => 'Tamano',
|
|
150
|
+
'path' => 'Ruta'
|
|
151
|
+
},
|
|
152
|
+
'empty' => {
|
|
153
|
+
'tree' => 'Sin resultados con los filtros actuales.',
|
|
154
|
+
'top_files' => 'Sin archivos para mostrar con los filtros actuales.'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}.freeze
|
|
159
|
+
|
|
160
|
+
module_function
|
|
161
|
+
|
|
162
|
+
def supported_locale?(value)
|
|
163
|
+
SUPPORTED_LOCALES.include?(normalize_locale(value))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def resolve_locale(cli_value: nil, env_lang: nil)
|
|
167
|
+
normalize_locale(cli_value) || normalize_locale(env_lang) || 'en'
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def normalize_locale(value)
|
|
171
|
+
return nil if value.nil?
|
|
172
|
+
|
|
173
|
+
normalized = value.to_s.strip.downcase
|
|
174
|
+
return nil if normalized.empty?
|
|
175
|
+
|
|
176
|
+
return 'es' if normalized.start_with?('es')
|
|
177
|
+
return 'en' if normalized.start_with?('en')
|
|
178
|
+
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def t(locale, key, **vars)
|
|
183
|
+
template = lookup(locale, key) || lookup('en', key)
|
|
184
|
+
return key unless template.is_a?(String)
|
|
185
|
+
|
|
186
|
+
return template if vars.empty?
|
|
187
|
+
|
|
188
|
+
template % vars.transform_keys(&:to_sym)
|
|
189
|
+
rescue KeyError
|
|
190
|
+
template
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def lookup(locale, key)
|
|
194
|
+
current = TRANSLATIONS[locale]
|
|
195
|
+
return nil unless current
|
|
196
|
+
|
|
197
|
+
key.to_s.split('.').each do |part|
|
|
198
|
+
return nil unless current.is_a?(Hash)
|
|
199
|
+
|
|
200
|
+
current = current[part]
|
|
201
|
+
end
|
|
202
|
+
current
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
5
|
|
|
6
6
|
module FileTreeVisualizer
|
|
7
7
|
class ReportBuilder
|
|
8
|
-
def initialize(result:, source_path:, generated_at: Time.now.utc)
|
|
8
|
+
def initialize(result:, source_path:, generated_at: Time.now.utc, locale: 'en')
|
|
9
9
|
@result = result
|
|
10
10
|
@source_path = File.expand_path(source_path.to_s)
|
|
11
11
|
@generated_at = generated_at
|
|
12
|
+
@locale = I18n.resolve_locale(cli_value: locale)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def build
|
|
15
|
-
payload_json = JSON.generate(payload).gsub(
|
|
16
|
+
payload_json = JSON.generate(payload).gsub('</', '<\\/')
|
|
17
|
+
translations_json = JSON.generate(translation_payload).gsub('</', '<\\/')
|
|
16
18
|
<<~HTML
|
|
17
19
|
<!doctype html>
|
|
18
|
-
<html lang="
|
|
20
|
+
<html lang="#{@locale}">
|
|
19
21
|
<head>
|
|
20
22
|
<meta charset="utf-8" />
|
|
21
23
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
22
|
-
<title
|
|
24
|
+
<title>#{t('report.page_title')}</title>
|
|
23
25
|
<style>
|
|
24
26
|
:root {
|
|
25
27
|
color-scheme: dark;
|
|
@@ -215,6 +217,7 @@ module FileTreeVisualizer
|
|
|
215
217
|
<script>
|
|
216
218
|
(function () {
|
|
217
219
|
const data = JSON.parse(document.getElementById("scan-data").textContent);
|
|
220
|
+
const i18n = #{translations_json};
|
|
218
221
|
const app = document.getElementById("app");
|
|
219
222
|
|
|
220
223
|
const state = {
|
|
@@ -256,31 +259,31 @@ module FileTreeVisualizer
|
|
|
256
259
|
'<div class="container">',
|
|
257
260
|
' <header class="header">',
|
|
258
261
|
' <div>',
|
|
259
|
-
' <h1>
|
|
262
|
+
' <h1>' + i18n.headerTitle + '</h1>',
|
|
260
263
|
' <div class="meta" id="meta-source"></div>',
|
|
261
264
|
' <div class="meta" id="meta-generated"></div>',
|
|
262
265
|
' </div>',
|
|
263
266
|
' </header>',
|
|
264
267
|
' <section class="stats">',
|
|
265
|
-
' <div class="stat"><div class="stat-label">
|
|
266
|
-
' <div class="stat"><div class="stat-label">
|
|
267
|
-
' <div class="stat"><div class="stat-label">
|
|
268
|
-
' <div class="stat"><div class="stat-label">
|
|
268
|
+
' <div class="stat"><div class="stat-label">' + i18n.statsTotalSize + '</div><div class="stat-value" id="stat-bytes"></div></div>',
|
|
269
|
+
' <div class="stat"><div class="stat-label">' + i18n.statsFiles + '</div><div class="stat-value" id="stat-files"></div></div>',
|
|
270
|
+
' <div class="stat"><div class="stat-label">' + i18n.statsDirectories + '</div><div class="stat-value" id="stat-directories"></div></div>',
|
|
271
|
+
' <div class="stat"><div class="stat-label">' + i18n.statsSkipped + '</div><div class="stat-value" id="stat-skipped"></div></div>',
|
|
269
272
|
' </section>',
|
|
270
273
|
' <section class="filters">',
|
|
271
|
-
' <label>
|
|
272
|
-
' <label>
|
|
274
|
+
' <label>' + i18n.searchLabel + '<input id="query-input" type="text" placeholder="' + i18n.searchPlaceholder + '" /></label>',
|
|
275
|
+
' <label>' + i18n.minSizeLabel + '<input id="min-size-input" type="number" min="0" value="0" /><span class="hint">' + i18n.minSizeHint + '</span></label>',
|
|
273
276
|
' </section>',
|
|
274
277
|
' <section class="layout">',
|
|
275
278
|
' <article class="panel">',
|
|
276
|
-
' <h2
|
|
279
|
+
' <h2>' + i18n.treeTitle + '</h2>',
|
|
277
280
|
' <div class="panel-body" id="tree-panel"></div>',
|
|
278
281
|
' </article>',
|
|
279
282
|
' <article class="panel">',
|
|
280
|
-
' <h2>
|
|
283
|
+
' <h2>' + i18n.topFilesTitle + '</h2>',
|
|
281
284
|
' <div class="panel-body">',
|
|
282
285
|
' <table>',
|
|
283
|
-
' <thead><tr><th
|
|
286
|
+
' <thead><tr><th>' + i18n.tableIndex + '</th><th>' + i18n.tableSize + '</th><th>' + i18n.tablePath + '</th></tr></thead>',
|
|
284
287
|
' <tbody id="top-files-body"></tbody>',
|
|
285
288
|
' </table>',
|
|
286
289
|
' </div>',
|
|
@@ -294,8 +297,8 @@ module FileTreeVisualizer
|
|
|
294
297
|
const treePanel = document.getElementById("tree-panel");
|
|
295
298
|
const topFilesBody = document.getElementById("top-files-body");
|
|
296
299
|
|
|
297
|
-
document.getElementById("meta-source").textContent =
|
|
298
|
-
document.getElementById("meta-generated").textContent =
|
|
300
|
+
document.getElementById("meta-source").textContent = i18n.metaSourcePrefix + data.source_path;
|
|
301
|
+
document.getElementById("meta-generated").textContent = i18n.metaGeneratedPrefix + data.generated_at;
|
|
299
302
|
document.getElementById("stat-bytes").textContent = formatBytes(data.stats.bytes);
|
|
300
303
|
document.getElementById("stat-files").textContent = formatNumber(data.stats.files);
|
|
301
304
|
document.getElementById("stat-directories").textContent = formatNumber(data.stats.directories);
|
|
@@ -365,7 +368,7 @@ module FileTreeVisualizer
|
|
|
365
368
|
if (!nodeMatches(data.root, activeFilters.query, activeFilters.minBytes)) {
|
|
366
369
|
const empty = document.createElement("div");
|
|
367
370
|
empty.className = "empty-state";
|
|
368
|
-
empty.textContent =
|
|
371
|
+
empty.textContent = i18n.emptyTree;
|
|
369
372
|
treePanel.appendChild(empty);
|
|
370
373
|
return;
|
|
371
374
|
}
|
|
@@ -384,7 +387,7 @@ module FileTreeVisualizer
|
|
|
384
387
|
const cell = document.createElement("td");
|
|
385
388
|
cell.colSpan = 3;
|
|
386
389
|
cell.className = "empty-state";
|
|
387
|
-
cell.textContent =
|
|
390
|
+
cell.textContent = i18n.emptyTopFiles;
|
|
388
391
|
emptyRow.appendChild(cell);
|
|
389
392
|
topFilesBody.appendChild(emptyRow);
|
|
390
393
|
return;
|
|
@@ -439,5 +442,32 @@ module FileTreeVisualizer
|
|
|
439
442
|
top_files: @result.top_files
|
|
440
443
|
}
|
|
441
444
|
end
|
|
445
|
+
|
|
446
|
+
def translation_payload
|
|
447
|
+
{
|
|
448
|
+
headerTitle: t('report.header_title'),
|
|
449
|
+
metaSourcePrefix: t('report.meta_source_prefix'),
|
|
450
|
+
metaGeneratedPrefix: t('report.meta_generated_prefix'),
|
|
451
|
+
statsTotalSize: t('report.stats.total_size'),
|
|
452
|
+
statsFiles: t('report.stats.files'),
|
|
453
|
+
statsDirectories: t('report.stats.directories'),
|
|
454
|
+
statsSkipped: t('report.stats.skipped'),
|
|
455
|
+
searchLabel: t('report.filters.search_label'),
|
|
456
|
+
searchPlaceholder: t('report.filters.search_placeholder'),
|
|
457
|
+
minSizeLabel: t('report.filters.min_size_label'),
|
|
458
|
+
minSizeHint: t('report.filters.min_size_hint'),
|
|
459
|
+
treeTitle: t('report.panels.tree'),
|
|
460
|
+
topFilesTitle: t('report.panels.top_files'),
|
|
461
|
+
tableIndex: t('report.table.index'),
|
|
462
|
+
tableSize: t('report.table.size'),
|
|
463
|
+
tablePath: t('report.table.path'),
|
|
464
|
+
emptyTree: t('report.empty.tree'),
|
|
465
|
+
emptyTopFiles: t('report.empty.top_files')
|
|
466
|
+
}
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def t(key)
|
|
470
|
+
I18n.t(@locale, key)
|
|
471
|
+
end
|
|
442
472
|
end
|
|
443
473
|
end
|
|
@@ -5,16 +5,19 @@ module FileTreeVisualizer
|
|
|
5
5
|
DEFAULT_TOP_LIMIT = 200
|
|
6
6
|
Result = Struct.new(:root, :top_files, :stats, keyword_init: true)
|
|
7
7
|
|
|
8
|
-
def initialize(top_limit: DEFAULT_TOP_LIMIT, follow_symlinks: false)
|
|
8
|
+
def initialize(top_limit: DEFAULT_TOP_LIMIT, follow_symlinks: false, excludes: [])
|
|
9
9
|
@top_limit = top_limit
|
|
10
10
|
@follow_symlinks = follow_symlinks
|
|
11
|
+
@excludes = Array(excludes).map(&:to_s).reject(&:empty?)
|
|
11
12
|
reset!
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def scan(path, progress: nil)
|
|
15
16
|
reset!
|
|
16
17
|
expanded_path = File.expand_path(path.to_s)
|
|
17
|
-
raise ArgumentError, "
|
|
18
|
+
raise ArgumentError, "Path is not a directory: #{expanded_path}" unless File.directory?(expanded_path)
|
|
19
|
+
|
|
20
|
+
@root_path = expanded_path
|
|
18
21
|
|
|
19
22
|
@progress_callback = progress
|
|
20
23
|
notify_counting_progress(force: true)
|
|
@@ -57,6 +60,7 @@ module FileTreeVisualizer
|
|
|
57
60
|
@processed_directories = 0
|
|
58
61
|
@counted_files = 0
|
|
59
62
|
@counted_directories = 0
|
|
63
|
+
@root_path = nil
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
def scan_directory(path)
|
|
@@ -66,7 +70,7 @@ module FileTreeVisualizer
|
|
|
66
70
|
node = {
|
|
67
71
|
name: directory_name,
|
|
68
72
|
path: path,
|
|
69
|
-
type:
|
|
73
|
+
type: 'directory',
|
|
70
74
|
size: 0,
|
|
71
75
|
children: []
|
|
72
76
|
}
|
|
@@ -78,6 +82,11 @@ module FileTreeVisualizer
|
|
|
78
82
|
entries = Dir.children(path)
|
|
79
83
|
entries.each do |entry_name|
|
|
80
84
|
child_path = File.join(path, entry_name)
|
|
85
|
+
if excluded_path?(child_path)
|
|
86
|
+
@skipped_count += 1
|
|
87
|
+
next
|
|
88
|
+
end
|
|
89
|
+
|
|
81
90
|
process_child_node(child_path, entry_name, node)
|
|
82
91
|
rescue StandardError
|
|
83
92
|
@skipped_count += 1
|
|
@@ -106,7 +115,7 @@ module FileTreeVisualizer
|
|
|
106
115
|
file_node = {
|
|
107
116
|
name: entry_name,
|
|
108
117
|
path: child_path,
|
|
109
|
-
type:
|
|
118
|
+
type: 'file',
|
|
110
119
|
size: file_size
|
|
111
120
|
}
|
|
112
121
|
parent_node[:children] << file_node
|
|
@@ -142,6 +151,7 @@ module FileTreeVisualizer
|
|
|
142
151
|
entries = Dir.children(path)
|
|
143
152
|
entries.each do |entry_name|
|
|
144
153
|
child_path = File.join(path, entry_name)
|
|
154
|
+
next if excluded_path?(child_path)
|
|
145
155
|
|
|
146
156
|
begin
|
|
147
157
|
if File.symlink?(child_path) && !@follow_symlinks
|
|
@@ -165,6 +175,22 @@ module FileTreeVisualizer
|
|
|
165
175
|
totals
|
|
166
176
|
end
|
|
167
177
|
|
|
178
|
+
def excluded_path?(path)
|
|
179
|
+
return false if @excludes.empty?
|
|
180
|
+
|
|
181
|
+
basename = File.basename(path)
|
|
182
|
+
relative = relative_path(path)
|
|
183
|
+
flags = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
|
|
184
|
+
|
|
185
|
+
@excludes.any? do |pattern|
|
|
186
|
+
File.fnmatch?(pattern, basename, flags) || File.fnmatch?(pattern, relative, flags)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def relative_path(path)
|
|
191
|
+
path.delete_prefix("#{@root_path}/")
|
|
192
|
+
end
|
|
193
|
+
|
|
168
194
|
def notify_progress
|
|
169
195
|
return unless @progress_callback
|
|
170
196
|
|
data/lib/file_tree_visualizer.rb
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
3
|
+
require_relative 'file_tree_visualizer/version'
|
|
4
|
+
require_relative 'file_tree_visualizer/i18n'
|
|
5
|
+
require_relative 'file_tree_visualizer/scanner'
|
|
6
|
+
require_relative 'file_tree_visualizer/report_builder'
|
|
7
|
+
require_relative 'file_tree_visualizer/cli'
|
|
7
8
|
|
|
8
9
|
module FileTreeVisualizer
|
|
9
10
|
end
|
metadata
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: file_tree_visualizer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- John Bidwell
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-04-04 00:00:00.000000000 Z
|
|
12
|
-
dependencies:
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: csv
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.3'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.3'
|
|
13
27
|
description: Scans a directory recursively, computes file/directory sizes, and generates
|
|
14
28
|
an HTML report rendered with React.
|
|
15
29
|
email:
|
|
16
|
-
-
|
|
30
|
+
- johnbidwellb@gmail.com
|
|
17
31
|
executables:
|
|
18
32
|
- file-tree-visualizer
|
|
19
33
|
extensions: []
|
|
@@ -26,15 +40,17 @@ files:
|
|
|
26
40
|
- file_tree_visualizer.gemspec
|
|
27
41
|
- lib/file_tree_visualizer.rb
|
|
28
42
|
- lib/file_tree_visualizer/cli.rb
|
|
43
|
+
- lib/file_tree_visualizer/i18n.rb
|
|
29
44
|
- lib/file_tree_visualizer/report_builder.rb
|
|
30
45
|
- lib/file_tree_visualizer/scanner.rb
|
|
31
46
|
- lib/file_tree_visualizer/version.rb
|
|
32
|
-
homepage: https://
|
|
47
|
+
homepage: https://github.com/JohnBidwellB/file-tree-visualizer
|
|
33
48
|
licenses:
|
|
34
49
|
- MIT
|
|
35
50
|
metadata:
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
source_code_uri: https://github.com/JohnBidwellB/file-tree-visualizer/tree/main
|
|
52
|
+
bug_tracker_uri: https://github.com/JohnBidwellB/file-tree-visualizer/issues
|
|
53
|
+
changelog_uri: https://github.com/JohnBidwellB/file-tree-visualizer/blob/main/CHANGELOG.md
|
|
38
54
|
post_install_message:
|
|
39
55
|
rdoc_options: []
|
|
40
56
|
require_paths:
|