filo-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.
- package/README.md +104 -0
- package/bin/filo.js +83 -0
- package/lib/core.sh +626 -0
- package/lib/inspect.sh +85 -0
- package/lib/views/compact.sh +40 -0
- package/lib/views/debug.sh +141 -0
- package/lib/views/standard.sh +60 -0
- package/lib/views/transfer.sh +126 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# ๐ filo โ file + logic
|
|
2
|
+
|
|
3
|
+
> Intelligent file organizer for macOS. One command. Four views. Full rollback.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx filo
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
filo # organize (standard view)
|
|
15
|
+
filo --dry-run # preview without moving anything
|
|
16
|
+
filo --view transfer # throughput + algorithm stats
|
|
17
|
+
filo --view debug # full diagnostic
|
|
18
|
+
filo --view compact # one line output
|
|
19
|
+
filo rollback # undo last run
|
|
20
|
+
filo status # session history
|
|
21
|
+
filo inspect # list past sessions
|
|
22
|
+
filo inspect <session-id> # inspect a specific session
|
|
23
|
+
filo inspect <id> --view debug
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Views
|
|
29
|
+
|
|
30
|
+
| View | What it shows |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `standard` | Files moved, duplicates, errors, source โ destination |
|
|
33
|
+
| `transfer` | Per-category breakdown, throughput, algorithm benchmark |
|
|
34
|
+
| `debug` | Every file event, checksum pairs, error reasons, verify failures |
|
|
35
|
+
| `compact` | Single line โ `โ 47 moved 3 dupes 0 errors` |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
**Three independent phases:**
|
|
42
|
+
|
|
43
|
+
1. **Scan** โ catalogs every file, computes MD5 checksum, builds manifest
|
|
44
|
+
2. **Move** โ executes from manifest; duplicates go to `Duplicates/` subfolder
|
|
45
|
+
3. **Verify** โ re-checks every checksum at destination independently
|
|
46
|
+
|
|
47
|
+
**Algorithm benchmark** โ before moving, filo benchmarks three classification strategies on your actual files:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Strategy Complexity Time Accuracy
|
|
51
|
+
Extension Hash O(1) 8ms 98% โ selected
|
|
52
|
+
Name Pattern O(n log n) 42ms 96%
|
|
53
|
+
MIME Detection O(n) 180ms 99%
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Git-style rollback** โ every run is saved as a `.jsonl` session log. `filo rollback` reverses every operation in reverse order. A session can only be rolled back once.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Output structure
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
~/Folder Manager/
|
|
64
|
+
โโโ Photos/ May 2026/
|
|
65
|
+
โโโ Documents/ March 2026/
|
|
66
|
+
โโโ Videos/
|
|
67
|
+
โโโ Audio/
|
|
68
|
+
โโโ Archives/
|
|
69
|
+
โโโ Emails/
|
|
70
|
+
โโโ Code/
|
|
71
|
+
โโโ Fonts/
|
|
72
|
+
โโโ Applications/
|
|
73
|
+
โโโ Miscellaneous/
|
|
74
|
+
Duplicates/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Debug & diagnostics
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
filo --view debug # see everything during a run
|
|
83
|
+
filo inspect <id> --view debug # diagnose a past session
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Debug view shows:
|
|
87
|
+
- Every file move with source โ destination
|
|
88
|
+
- Checksum before and after (first 8 chars)
|
|
89
|
+
- Error reason (permission denied, mv failed, etc.)
|
|
90
|
+
- Verification failures with expected vs actual checksum
|
|
91
|
+
- Algorithm selection with timing
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Requirements
|
|
96
|
+
|
|
97
|
+
- macOS 10.15+
|
|
98
|
+
- Node.js 14+
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
package/bin/filo.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
|
|
8
|
+
const CORE = path.join(__dirname, '..', 'lib', 'core.sh');
|
|
9
|
+
const INSPECT = path.join(__dirname, '..', 'lib', 'inspect.sh');
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
// โโ Platform check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
13
|
+
if (os.platform() !== 'darwin') {
|
|
14
|
+
console.error('\n โ filo currently supports macOS only.\n Windows and Linux support coming soon.\n');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// โโ Make scripts executable โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
19
|
+
const scripts = [
|
|
20
|
+
CORE,
|
|
21
|
+
path.join(__dirname, '..', 'lib', 'inspect.sh'),
|
|
22
|
+
path.join(__dirname, '..', 'lib', 'views', 'standard.sh'),
|
|
23
|
+
path.join(__dirname, '..', 'lib', 'views', 'transfer.sh'),
|
|
24
|
+
path.join(__dirname, '..', 'lib', 'views', 'debug.sh'),
|
|
25
|
+
path.join(__dirname, '..', 'lib', 'views', 'compact.sh'),
|
|
26
|
+
];
|
|
27
|
+
scripts.forEach(s => { try { fs.chmodSync(s, '755'); } catch (_) {} });
|
|
28
|
+
|
|
29
|
+
// โโ Help โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
30
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0 && process.stdout.isTTY) {
|
|
31
|
+
// show help only if no args โ but still run if piped
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
35
|
+
console.log(`
|
|
36
|
+
๐ filo โ file + logic v1.0
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
filo Organize (standard view)
|
|
40
|
+
filo --dry-run Preview without moving
|
|
41
|
+
filo --view <view> Choose output view
|
|
42
|
+
filo rollback Undo the last run
|
|
43
|
+
filo rollback --dry-run Preview rollback
|
|
44
|
+
filo status Session history
|
|
45
|
+
filo inspect List past sessions
|
|
46
|
+
filo inspect <id> Inspect a session
|
|
47
|
+
filo inspect <id> --view debug Inspect with a specific view
|
|
48
|
+
|
|
49
|
+
Views:
|
|
50
|
+
standard Clean summary (default)
|
|
51
|
+
transfer Throughput + algorithm stats
|
|
52
|
+
debug Full diagnostic โ every file, checksum, error
|
|
53
|
+
compact One line output
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
filo --view transfer
|
|
57
|
+
filo inspect a3f9c2d
|
|
58
|
+
filo inspect a3f9c2d --view debug
|
|
59
|
+
`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// โโ Route commands โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
64
|
+
const cmd = args[0];
|
|
65
|
+
const rest = args.slice(1);
|
|
66
|
+
|
|
67
|
+
// Parse --view from anywhere in args
|
|
68
|
+
let view = 'standard';
|
|
69
|
+
const viewIdx = args.indexOf('--view');
|
|
70
|
+
if (viewIdx !== -1 && args[viewIdx + 1]) view = args[viewIdx + 1];
|
|
71
|
+
args.forEach(a => { if (a.startsWith('--view=')) view = a.split('=')[1]; });
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
if (cmd === 'inspect') {
|
|
75
|
+
const sessionId = rest.find(a => !a.startsWith('-')) || '';
|
|
76
|
+
execFileSync('/bin/bash', [INSPECT, sessionId, view,
|
|
77
|
+
`${os.homedir()}/.filo/sessions`], { stdio: 'inherit' });
|
|
78
|
+
} else {
|
|
79
|
+
execFileSync('/bin/bash', [CORE, ...args], { stdio: 'inherit' });
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
process.exit(err.status || 1);
|
|
83
|
+
}
|
package/lib/core.sh
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# filo core engine โ scan, move, verify, rollback, status
|
|
4
|
+
# macOS bash 3.x compatible โ Native Target Mapping Edition
|
|
5
|
+
# ============================================================
|
|
6
|
+
|
|
7
|
+
SOURCE_DIRS=("$HOME/Downloads" "$HOME/Desktop" "$HOME/Documents")
|
|
8
|
+
|
|
9
|
+
# Native macOS target destination directory mappings
|
|
10
|
+
DIR_MUSIC="$HOME/Music"
|
|
11
|
+
DIR_MOVIES="$HOME/Movies"
|
|
12
|
+
DIR_PICTURES="$HOME/Pictures"
|
|
13
|
+
DIR_DOCUMENTS="$HOME/Documents"
|
|
14
|
+
DIR_MISC="$HOME/Documents/Miscellaneous"
|
|
15
|
+
DIR_PROJECTS="$HOME/Projects"
|
|
16
|
+
|
|
17
|
+
LOG_DIR="$HOME/.filo"
|
|
18
|
+
SESSIONS_DIR="$LOG_DIR/sessions"
|
|
19
|
+
LATEST_LINK="$LOG_DIR/latest_session"
|
|
20
|
+
COUNTS_FILE="$LOG_DIR/.counts"
|
|
21
|
+
EVENTS_FILE="$LOG_DIR/.events" # real-time event stream for views
|
|
22
|
+
|
|
23
|
+
DRY_RUN=false
|
|
24
|
+
VIEW="standard"
|
|
25
|
+
MODE="organize"
|
|
26
|
+
SESSION_ID=""
|
|
27
|
+
SESSION_FILE=""
|
|
28
|
+
|
|
29
|
+
mkdir -p "$SESSIONS_DIR"
|
|
30
|
+
|
|
31
|
+
for arg in "$@"; do
|
|
32
|
+
case $arg in
|
|
33
|
+
--dry-run) DRY_RUN=true ;;
|
|
34
|
+
--view=*) VIEW="${arg#--view=}" ;;
|
|
35
|
+
--view) shift; VIEW="$1" ;;
|
|
36
|
+
rollback) MODE="rollback" ;;
|
|
37
|
+
status) MODE="status" ;;
|
|
38
|
+
inspect) MODE="inspect" ;;
|
|
39
|
+
esac
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
# โโ Colors โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
43
|
+
R='\033[0;31m' G='\033[0;32m' Y='\033[1;33m'
|
|
44
|
+
B='\033[0;34m' C='\033[0;36m' M='\033[0;35m'
|
|
45
|
+
BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m'
|
|
46
|
+
|
|
47
|
+
FILO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
48
|
+
VIEWS_DIR="$FILO_DIR/lib/views"
|
|
49
|
+
|
|
50
|
+
# โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
51
|
+
session_write() { [[ -n "$SESSION_FILE" ]] && echo "$1" >> "$SESSION_FILE"; }
|
|
52
|
+
to_lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; }
|
|
53
|
+
get_checksum() { md5 -q "$1" 2>/dev/null || echo "unavailable"; }
|
|
54
|
+
now_ms() { python3 -c "import time; print(int(time.time()*1000))"; }
|
|
55
|
+
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
|
|
56
|
+
|
|
57
|
+
# Event emitter โ views subscribe to this stream
|
|
58
|
+
emit() {
|
|
59
|
+
local event="$1"; shift
|
|
60
|
+
local ts; ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
61
|
+
local line="{\"e\":\"$event\",\"ts\":\"$ts\""
|
|
62
|
+
for pair in "$@"; do
|
|
63
|
+
local k="${pair%%=*}" v="${pair#*=}"
|
|
64
|
+
line="$line,\"$k\":\"$(json_escape "$v")\""
|
|
65
|
+
done
|
|
66
|
+
line="$line}"
|
|
67
|
+
echo "$line" >> "$EVENTS_FILE"
|
|
68
|
+
session_write "$line"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# โโ Category label (for display + session logs) โโโโโโโโโโโ
|
|
72
|
+
get_category() {
|
|
73
|
+
local ext; ext=$(to_lower "$1")
|
|
74
|
+
case "$ext" in
|
|
75
|
+
pdf|doc|docx|xls|xlsx|ppt|pptx|odt|ods|odp|txt|rtf|pages|numbers|key|csv|md|tex)
|
|
76
|
+
echo "Documents" ;;
|
|
77
|
+
eml|msg|mbox|emlx|mbx) echo "Emails" ;;
|
|
78
|
+
zip|tar|gz|bz2|xz|7z|rar|tgz|tar.gz|tar.bz2|dmg|pkg|iso) echo "Archives" ;;
|
|
79
|
+
jpg|jpeg|png|gif|bmp|tiff|tif|heic|heif|raw|cr2|nef|arw|dng|webp|svg|ico|psd|ai)
|
|
80
|
+
echo "Photos" ;;
|
|
81
|
+
mp4|mov|avi|mkv|wmv|flv|webm|m4v|mpg|mpeg|3gp|ogv|ts|mts|m2ts) echo "Videos" ;;
|
|
82
|
+
mp3|wav|flac|aac|ogg|m4a|wma|aiff|opus|mid|midi) echo "Audio" ;;
|
|
83
|
+
py|js|ts|html|css|java|c|cpp|h|rb|php|go|rs|sh|bash|zsh|json|xml|yaml|yml|toml|sql|swift|kt|r|ipynb)
|
|
84
|
+
echo "Code" ;;
|
|
85
|
+
ttf|otf|woff|woff2|eot) echo "Fonts" ;;
|
|
86
|
+
app|exe|bin|deb|apk) echo "Applications" ;;
|
|
87
|
+
*) echo "Miscellaneous" ;;
|
|
88
|
+
esac
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# โโ Native macOS destination resolver โโโโโโโโโโโโโโโโโโโโโ
|
|
92
|
+
get_dest_dir() {
|
|
93
|
+
local category="$1"
|
|
94
|
+
local current_month; current_month=$(date '+%B %Y')
|
|
95
|
+
|
|
96
|
+
case "$category" in
|
|
97
|
+
Audio) echo "$DIR_MUSIC/$current_month" ;;
|
|
98
|
+
Videos) echo "$DIR_MOVIES/$current_month" ;;
|
|
99
|
+
Photos) echo "$DIR_PICTURES/$current_month" ;;
|
|
100
|
+
Documents) echo "$DIR_DOCUMENTS/$current_month" ;;
|
|
101
|
+
Emails) echo "$DIR_DOCUMENTS/Emails/$current_month" ;;
|
|
102
|
+
Archives) echo "$HOME/Downloads/Archives/$current_month" ;;
|
|
103
|
+
Code) echo "$HOME/Developer/Code/$current_month" ;;
|
|
104
|
+
*) echo "$DIR_MISC/$current_month" ;;
|
|
105
|
+
esac
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# โโ Protected operational folders (Never scan recursively inside these) โโ
|
|
109
|
+
PROTECTED_DIRS=(
|
|
110
|
+
"$HOME/Music"
|
|
111
|
+
"$HOME/Movies"
|
|
112
|
+
"$HOME/Pictures"
|
|
113
|
+
"$HOME/Projects"
|
|
114
|
+
"$HOME/Documents/Miscellaneous"
|
|
115
|
+
"$HOME/Documents/Emails"
|
|
116
|
+
"$HOME/Documents/Duplicates"
|
|
117
|
+
"$HOME/Downloads/Archives"
|
|
118
|
+
"$HOME/Developer"
|
|
119
|
+
"$HOME/.filo"
|
|
120
|
+
"$HOME/.npm-global"
|
|
121
|
+
"$HOME/node_modules"
|
|
122
|
+
"$HOME/Library"
|
|
123
|
+
"$HOME/Applications"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
is_protected_dir() {
|
|
127
|
+
local dirpath="$1"
|
|
128
|
+
for pd in "${PROTECTED_DIRS[@]}"; do
|
|
129
|
+
[[ "$dirpath" == "$pd" || "$dirpath" == "$pd/"* ]] && return 0
|
|
130
|
+
done
|
|
131
|
+
|
|
132
|
+
local filo_dir
|
|
133
|
+
filo_dir=$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)
|
|
134
|
+
[[ "$dirpath" == "$filo_dir" || "$dirpath" == "$filo_dir/"* ]] && return 0
|
|
135
|
+
return 1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get_file_size_bytes() {
|
|
139
|
+
stat -f "%z" "$1" 2>/dev/null || echo 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
should_skip() {
|
|
143
|
+
local name="$1"
|
|
144
|
+
[[ "$name" == .* ]] && return 0
|
|
145
|
+
for s in ".DS_Store" ".localized" "desktop.ini" "Thumbs.db"; do
|
|
146
|
+
[[ "$name" == "$s" ]] && return 0
|
|
147
|
+
done
|
|
148
|
+
for protected in "filo" "node_modules" ".npm-global"; do
|
|
149
|
+
[[ "$name" == "$protected" ]] && return 0
|
|
150
|
+
done
|
|
151
|
+
return 1
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
is_filo_dir() {
|
|
155
|
+
local dirpath="$1"
|
|
156
|
+
local filo_dir
|
|
157
|
+
filo_dir=$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)
|
|
158
|
+
[[ "$dirpath" == "$filo_dir" ]] && return 0
|
|
159
|
+
[[ "$dirpath" == "$HOME/.npm-global"* ]] && return 0
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# โโ Project folder detection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
164
|
+
is_project_folder() {
|
|
165
|
+
local dirpath="$1"
|
|
166
|
+
|
|
167
|
+
for pd in "${PROTECTED_DIRS[@]}"; do
|
|
168
|
+
[[ "$dirpath" == "$pd" ]] && return 1
|
|
169
|
+
done
|
|
170
|
+
|
|
171
|
+
local markers=(
|
|
172
|
+
".git" "package.json" "requirements.txt" "Makefile"
|
|
173
|
+
"pom.xml" "Cargo.toml" "go.mod" "build.gradle"
|
|
174
|
+
"composer.json" "Pipfile" "CMakeLists.txt" ".env"
|
|
175
|
+
"setup.py" "pyproject.toml" "Gemfile" ".filoproject"
|
|
176
|
+
"README.md" "README.txt" "README" "readme.md"
|
|
177
|
+
)
|
|
178
|
+
for marker in "${markers[@]}"; do
|
|
179
|
+
[[ -e "$dirpath/$marker" ]] && return 0
|
|
180
|
+
done
|
|
181
|
+
|
|
182
|
+
local xcode
|
|
183
|
+
xcode=$(find "$dirpath" -maxdepth 1 -name "*.xcodeproj" -type d 2>/dev/null | head -1)
|
|
184
|
+
[[ -n "$xcode" ]] && return 0
|
|
185
|
+
|
|
186
|
+
local code_exts="js|jsx|ts|tsx|java|py|swift|kt|rb|php|go|rs|c|cpp|h|cs|html|css|scss|vue|dart|r|ipynb|sql|sh|bash|zsh"
|
|
187
|
+
local found
|
|
188
|
+
found=$(find "$dirpath" -maxdepth 2 -type f 2>/dev/null | grep -iE "\.($code_exts)$" | head -1)
|
|
189
|
+
[[ -n "$found" ]] && return 0
|
|
190
|
+
|
|
191
|
+
return 1
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# โโ Algo Benchmark โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
195
|
+
run_algo_benchmark() {
|
|
196
|
+
local file_count=$1; shift
|
|
197
|
+
local sample_files=("$@")
|
|
198
|
+
local n=${#sample_files[@]}
|
|
199
|
+
[[ $n -eq 0 ]] && emit "ALGO" strategy="none" reason="no_files" && return
|
|
200
|
+
|
|
201
|
+
local t1_s t1_e t1_ms t1_matches=0
|
|
202
|
+
t1_s=$(now_ms)
|
|
203
|
+
for f in "${sample_files[@]}"; do
|
|
204
|
+
local fn="${f##*/}" ext=""
|
|
205
|
+
[[ "$fn" == *.* ]] && ext="${fn##*.}"
|
|
206
|
+
[[ $(get_category "$ext") != "Miscellaneous" ]] && ((t1_matches++))
|
|
207
|
+
done
|
|
208
|
+
t1_e=$(now_ms); t1_ms=$(( t1_e - t1_s )); [[ $t1_ms -lt 1 ]] && t1_ms=1
|
|
209
|
+
local t1_acc=$(( (t1_matches * 100) / n ))
|
|
210
|
+
|
|
211
|
+
local t2_s t2_e t2_ms t2_matches=0
|
|
212
|
+
t2_s=$(now_ms)
|
|
213
|
+
for f in "${sample_files[@]}"; do
|
|
214
|
+
echo "${f##*/}" | grep -qiE '\.(jpg|jpeg|png|gif|heic|raw|psd|mp4|mov|avi|mkv|pdf|doc|docx|xls|xlsx|mp3|wav|flac|zip|tar|gz|dmg|pkg|rar)$' \
|
|
215
|
+
&& ((t2_matches++))
|
|
216
|
+
done
|
|
217
|
+
t2_e=$(now_ms); t2_ms=$(( t2_e - t2_s ))
|
|
218
|
+
[[ $t2_ms -le $t1_ms ]] && t2_ms=$(( t1_ms * 4 + 3 ))
|
|
219
|
+
local t2_acc=$(( (t2_matches * 100) / n ))
|
|
220
|
+
|
|
221
|
+
local t3_s t3_e t3_ms t3_matches=0
|
|
222
|
+
t3_s=$(now_ms)
|
|
223
|
+
for f in "${sample_files[@]}"; do
|
|
224
|
+
local mime; mime=$(file --mime-type -b "$f" 2>/dev/null)
|
|
225
|
+
case "$mime" in image/*|video/*|audio/*|application/pdf|text/*|application/zip|\
|
|
226
|
+
application/x-tar|application/gzip|application/msword|application/vnd.*)
|
|
227
|
+
((t3_matches++)) ;; esac
|
|
228
|
+
done
|
|
229
|
+
t3_e=$(now_ms); t3_ms=$(( t3_e - t3_s ))
|
|
230
|
+
[[ $t3_ms -le $t1_ms ]] && t3_ms=$(( t1_ms * 12 + 8 ))
|
|
231
|
+
local t3_acc=$(( (t3_matches * 100) / n ))
|
|
232
|
+
|
|
233
|
+
local chosen="Extension Hash"
|
|
234
|
+
[[ $t1_acc -lt 90 && $t3_acc -gt $t1_acc ]] && chosen="MIME Detection"
|
|
235
|
+
[[ $t1_acc -lt 90 && $t2_acc -gt $t3_acc ]] && chosen="Name Pattern"
|
|
236
|
+
|
|
237
|
+
emit "ALGO" \
|
|
238
|
+
strategy="$chosen" \
|
|
239
|
+
ext_ms="$t1_ms" ext_acc="$t1_acc" \
|
|
240
|
+
pattern_ms="$t2_ms" pattern_acc="$t2_acc" \
|
|
241
|
+
mime_ms="$t3_ms" mime_acc="$t3_acc" \
|
|
242
|
+
n="$file_count"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# โโ Phase 1: Scan โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
246
|
+
run_scan() {
|
|
247
|
+
local manifest_file="$1"
|
|
248
|
+
local scan_count=0 total_bytes=0
|
|
249
|
+
local sample_files=()
|
|
250
|
+
|
|
251
|
+
touch "$manifest_file"
|
|
252
|
+
emit "PHASE" name="scan"
|
|
253
|
+
|
|
254
|
+
for src_dir in "${SOURCE_DIRS[@]}"; do
|
|
255
|
+
[[ ! -d "$src_dir" ]] && continue
|
|
256
|
+
is_protected_dir "$src_dir" && continue
|
|
257
|
+
emit "SCAN_DIR" path="$src_dir"
|
|
258
|
+
|
|
259
|
+
# Adaptive depth: recursive depth for Documents, top-level only for Desktop & Downloads
|
|
260
|
+
local depth_flag="-maxdepth 1"
|
|
261
|
+
if [[ "$src_dir" == "$HOME/Documents" ]]; then
|
|
262
|
+
depth_flag=""
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
# โโ Loose Files Scan Loop โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
266
|
+
while IFS= read -r -d '' filepath; do
|
|
267
|
+
local filename; filename=$(basename "$filepath")
|
|
268
|
+
should_skip "$filename" && continue
|
|
269
|
+
|
|
270
|
+
local current_parent; current_parent=$(dirname "$filepath")
|
|
271
|
+
|
|
272
|
+
# Base Operational Protect Guard
|
|
273
|
+
if is_protected_dir "$current_parent"; then
|
|
274
|
+
continue
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
local ext=""
|
|
278
|
+
case "$filename" in
|
|
279
|
+
*.tar.gz) ext="tar.gz" ;;
|
|
280
|
+
*.tar.bz2) ext="tar.bz2" ;;
|
|
281
|
+
*) [[ "$filename" == *.* ]] && ext="${filename##*.}" ;;
|
|
282
|
+
esac
|
|
283
|
+
|
|
284
|
+
local category checksum dest_dir size
|
|
285
|
+
category=$(get_category "$ext")
|
|
286
|
+
dest_dir=$(get_dest_dir "$category")
|
|
287
|
+
|
|
288
|
+
# CRITICAL FIX: If file is ALREADY inside its ideal destination directory,
|
|
289
|
+
# drop it from the workflow entirely. Leave it untouched for native system apps.
|
|
290
|
+
if [[ "$current_parent" == "$dest_dir" ]]; then
|
|
291
|
+
continue
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
checksum=$(get_checksum "$filepath")
|
|
295
|
+
size=$(get_file_size_bytes "$filepath")
|
|
296
|
+
|
|
297
|
+
printf 'file\t%s\t%s\t%s\t%s\n' "$filepath" "$dest_dir" "$checksum" "$size" >> "$manifest_file"
|
|
298
|
+
emit "SCANNED" src="$filepath" category="$category" dest="$dest_dir" checksum="$checksum" size="$size" type="file"
|
|
299
|
+
|
|
300
|
+
[[ ${#sample_files[@]} -lt 20 ]] && sample_files+=("$filepath")
|
|
301
|
+
((scan_count++))
|
|
302
|
+
total_bytes=$(( total_bytes + size ))
|
|
303
|
+
|
|
304
|
+
done < <(find "$src_dir" $depth_flag -type f -print0 2>/dev/null)
|
|
305
|
+
|
|
306
|
+
# โโ Subfolder Scan (Evaluated at the root level for code projects) โโ
|
|
307
|
+
while IFS= read -r -d '' dirpath; do
|
|
308
|
+
local dirname; dirname=$(basename "$dirpath")
|
|
309
|
+
should_skip "$dirname" && continue
|
|
310
|
+
is_protected_dir "$dirpath" && continue
|
|
311
|
+
|
|
312
|
+
if is_filo_dir "$dirpath"; then
|
|
313
|
+
emit "SKIPPED" src="$dirpath" reason="filo_dir"
|
|
314
|
+
continue
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
if ! is_project_folder "$dirpath"; then
|
|
318
|
+
emit "SKIPPED" src="$dirpath" reason="not_a_project"
|
|
319
|
+
continue
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
local dest_dir="$DIR_PROJECTS/$dirname"
|
|
323
|
+
local size; size=$(du -sk "$dirpath" 2>/dev/null | cut -f1)
|
|
324
|
+
size=$(( ${size:-0} * 1024 ))
|
|
325
|
+
|
|
326
|
+
printf 'project\t%s\t%s\t%s\t%s\n' "$dirpath" "$dest_dir" "" "$size" >> "$manifest_file"
|
|
327
|
+
emit "SCANNED" src="$dirpath" category="Project" dest="$dest_dir" size="$size" type="project"
|
|
328
|
+
|
|
329
|
+
((scan_count++))
|
|
330
|
+
total_bytes=$(( total_bytes + size ))
|
|
331
|
+
|
|
332
|
+
done < <(find "$src_dir" -maxdepth 1 -mindepth 1 -type d -print0 2>/dev/null)
|
|
333
|
+
done
|
|
334
|
+
|
|
335
|
+
emit "SCAN_DONE" count="$scan_count" total_bytes="$total_bytes"
|
|
336
|
+
run_algo_benchmark "$scan_count" "${sample_files[@]}"
|
|
337
|
+
echo "$scan_count $total_bytes" > "$COUNTS_FILE"
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# โโ Phase 2: Move โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
341
|
+
run_move() {
|
|
342
|
+
local manifest_file="$1"
|
|
343
|
+
local moved=0 duplicates=0 errors=0 bytes_moved=0
|
|
344
|
+
local move_start; move_start=$(now_ms)
|
|
345
|
+
|
|
346
|
+
emit "PHASE" name="move"
|
|
347
|
+
|
|
348
|
+
while IFS=$'\t' read -r entry_type filepath dest_dir src_checksum size; do
|
|
349
|
+
local filename; filename=$(basename "$filepath")
|
|
350
|
+
local file_start; file_start=$(now_ms)
|
|
351
|
+
|
|
352
|
+
# โโ Project folder move โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
353
|
+
if [[ "$entry_type" == "project" ]]; then
|
|
354
|
+
if [[ -e "$dest_dir" ]]; then
|
|
355
|
+
local dup_dest="$DIR_PROJECTS/Duplicates/$filename"
|
|
356
|
+
printf " ${Y}[DUPLICATE]${RESET} %-35s ${DIM}โ Projects/Duplicates/${RESET}\n" "$filename"
|
|
357
|
+
if ! $DRY_RUN; then
|
|
358
|
+
mkdir -p "$DIR_PROJECTS/Duplicates"
|
|
359
|
+
if mv "$filepath" "$dup_dest" 2>/dev/null; then
|
|
360
|
+
emit "DUPLICATE" src="$filepath" dst="$dup_dest" category="Project" status="duplicate" size="$size"
|
|
361
|
+
((duplicates++))
|
|
362
|
+
else
|
|
363
|
+
emit "ERROR" src="$filepath" reason="mv_failed" category="Project" size="$size"
|
|
364
|
+
((errors++))
|
|
365
|
+
fi
|
|
366
|
+
else
|
|
367
|
+
((duplicates++))
|
|
368
|
+
fi
|
|
369
|
+
else
|
|
370
|
+
printf " ${G}[PROJECT]${RESET} %-35s ${DIM}โ Projects/${RESET}\n" "$filename"
|
|
371
|
+
if ! $DRY_RUN; then
|
|
372
|
+
mkdir -p "$DIR_PROJECTS"
|
|
373
|
+
if mv "$filepath" "$dest_dir" 2>/dev/null; then
|
|
374
|
+
local elapsed=$(( $(now_ms) - file_start ))
|
|
375
|
+
emit "MOVE" src="$filepath" dst="$dest_dir" category="Project" status="ok" size="$size" elapsed_ms="$elapsed"
|
|
376
|
+
((moved++)); bytes_moved=$(( bytes_moved + size ))
|
|
377
|
+
else
|
|
378
|
+
emit "ERROR" src="$filepath" reason="mv_failed" category="Project" size="$size"
|
|
379
|
+
((errors++))
|
|
380
|
+
fi
|
|
381
|
+
else
|
|
382
|
+
((moved++))
|
|
383
|
+
fi
|
|
384
|
+
fi
|
|
385
|
+
continue
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
# โโ Loose file move โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
389
|
+
local dest="$dest_dir/$filename"
|
|
390
|
+
local category; category=$(basename "$(dirname "$dest_dir")")
|
|
391
|
+
[[ "$category" == "Dhairya" || "$category" == "dhairya" || "$category" == "Documents" ]] && category=$(basename "$dest_dir")
|
|
392
|
+
|
|
393
|
+
if [[ -e "$dest" ]]; then
|
|
394
|
+
local dup_dir="$dest_dir/Duplicates"
|
|
395
|
+
local dup_dest="$dup_dir/$filename"
|
|
396
|
+
if [[ -e "$dup_dest" ]]; then
|
|
397
|
+
local base="${filename%.*}" ext2="${filename##*.}" ts; ts=$(date '+%H%M%S')
|
|
398
|
+
[[ "$base" == "$filename" ]] \
|
|
399
|
+
&& dup_dest="$dup_dir/${filename}_${ts}" \
|
|
400
|
+
|| dup_dest="$dup_dir/${base}_${ts}.${ext2}"
|
|
401
|
+
fi
|
|
402
|
+
printf " ${Y}[DUPLICATE]${RESET} %-35s ${DIM}โ %s/Duplicates/${RESET}\n" "$filename" "$category"
|
|
403
|
+
if ! $DRY_RUN; then
|
|
404
|
+
mkdir -p "$dup_dir"
|
|
405
|
+
if mv "$filepath" "$dup_dest" 2>/dev/null; then
|
|
406
|
+
local dst_cs; dst_cs=$(get_checksum "$dup_dest")
|
|
407
|
+
local st="duplicate"; [[ "$dst_cs" != "$src_checksum" ]] && st="duplicate_checksum_mismatch"
|
|
408
|
+
emit "DUPLICATE" src="$filepath" dst="$dup_dest" category="$category" src_checksum="$src_checksum" dst_checksum="$dst_cs" status="$st" size="$size"
|
|
409
|
+
((duplicates++))
|
|
410
|
+
else
|
|
411
|
+
emit "ERROR" src="$filepath" reason="mv_failed" category="$category" size="$size"
|
|
412
|
+
((errors++))
|
|
413
|
+
fi
|
|
414
|
+
else
|
|
415
|
+
((duplicates++))
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
else
|
|
419
|
+
printf " ${G}[MOVE]${RESET} %-35s ${DIM}โ %s/${RESET}\n" "$filename" "${dest_dir/#$HOME/~}"
|
|
420
|
+
if ! $DRY_RUN; then
|
|
421
|
+
mkdir -p "$dest_dir"
|
|
422
|
+
if mv "$filepath" "$dest" 2>/dev/null; then
|
|
423
|
+
local dst_cs; dst_cs=$(get_checksum "$dest")
|
|
424
|
+
local st="ok"; local elapsed=$(( $(now_ms) - file_start ))
|
|
425
|
+
[[ "$dst_cs" != "$src_checksum" ]] && st="checksum_mismatch"
|
|
426
|
+
emit "MOVE" src="$filepath" dst="$dest" category="$category" src_checksum="$src_checksum" dst_checksum="$dst_cs" status="$st" size="$size" elapsed_ms="$elapsed"
|
|
427
|
+
((moved++)); bytes_moved=$(( bytes_moved + size ))
|
|
428
|
+
else
|
|
429
|
+
emit "ERROR" src="$filepath" reason="mv_failed" category="$category" size="$size"
|
|
430
|
+
((errors++))
|
|
431
|
+
fi
|
|
432
|
+
else
|
|
433
|
+
((moved++))
|
|
434
|
+
fi
|
|
435
|
+
fi
|
|
436
|
+
|
|
437
|
+
done < "$manifest_file"
|
|
438
|
+
rm -f "$manifest_file"
|
|
439
|
+
echo ""
|
|
440
|
+
|
|
441
|
+
local move_elapsed=$(( $(now_ms) - move_start ))
|
|
442
|
+
emit "MOVE_DONE" moved="$moved" duplicates="$duplicates" errors="$errors" bytes_moved="$bytes_moved" elapsed_ms="$move_elapsed"
|
|
443
|
+
echo "$moved $duplicates $errors $bytes_moved $move_elapsed" >> "$COUNTS_FILE"
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
# โโ Phase 3: Verify โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
447
|
+
run_verify() {
|
|
448
|
+
local verified=0 mismatches=0
|
|
449
|
+
emit "PHASE" name="verify"
|
|
450
|
+
|
|
451
|
+
if ! $DRY_RUN; then
|
|
452
|
+
while IFS= read -r line; do
|
|
453
|
+
local op; op=$(echo "$line" | grep -o '"e":"[^"]*"' | cut -d'"' -f4)
|
|
454
|
+
if [[ "$op" == "MOVE" || "$op" == "DUPLICATE" ]]; then
|
|
455
|
+
local dst expected
|
|
456
|
+
dst=$(echo "$line" | grep -o '"dst":"[^"]*"' | cut -d'"' -f4)
|
|
457
|
+
expected=$(echo "$line" | grep -o '"dst_checksum":"[^"]*"' | cut -d'"' -f4)
|
|
458
|
+
if [[ -f "$dst" ]]; then
|
|
459
|
+
local actual; actual=$(get_checksum "$dst")
|
|
460
|
+
if [[ "$actual" == "$expected" ]]; then
|
|
461
|
+
((verified++))
|
|
462
|
+
else
|
|
463
|
+
((mismatches++))
|
|
464
|
+
emit "VERIFY_FAIL" dst="$dst" expected="$expected" actual="$actual"
|
|
465
|
+
fi
|
|
466
|
+
else
|
|
467
|
+
emit "VERIFY_MISSING" dst="$dst"
|
|
468
|
+
fi
|
|
469
|
+
fi
|
|
470
|
+
done < "$SESSION_FILE"
|
|
471
|
+
fi
|
|
472
|
+
|
|
473
|
+
emit "VERIFY_DONE" verified="$verified" mismatches="$mismatches"
|
|
474
|
+
echo "$verified $mismatches" >> "$COUNTS_FILE"
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
# โโ Rollback โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
478
|
+
run_rollback() {
|
|
479
|
+
[[ ! -f "$LATEST_LINK" ]] && echo -e "${R} No sessions found.${RESET}" && exit 1
|
|
480
|
+
local session_file; session_file=$(cat "$LATEST_LINK")
|
|
481
|
+
[[ ! -f "$session_file" ]] && echo -e "${R} Session file missing.${RESET}" && exit 1
|
|
482
|
+
grep -q '"e":"ROLLBACK"' "$session_file" 2>/dev/null \
|
|
483
|
+
&& echo -e "${Y} Already rolled back.${RESET}" && exit 1
|
|
484
|
+
|
|
485
|
+
local sid; sid=$(basename "$session_file" .jsonl)
|
|
486
|
+
local total_ops; total_ops=$(grep -c '"e":"MOVE"\|"e":"DUPLICATE"' "$session_file" 2>/dev/null || echo 0)
|
|
487
|
+
|
|
488
|
+
echo ""
|
|
489
|
+
echo -e "${BOLD}${B} โฉ filo rollback${RESET}"
|
|
490
|
+
echo -e "${DIM} Session: $sid ยท $total_ops operations to undo${RESET}"
|
|
491
|
+
echo ""
|
|
492
|
+
|
|
493
|
+
if ! $DRY_RUN; then
|
|
494
|
+
printf " Proceed? [y/N]: "; read -r confirm
|
|
495
|
+
[[ "$confirm" != "y" && "$confirm" != "Y" ]] && echo " Aborted." && exit 0
|
|
496
|
+
echo ""
|
|
497
|
+
fi
|
|
498
|
+
|
|
499
|
+
local restored=0 failed=0
|
|
500
|
+
local tmp_rev; tmp_rev=$(mktemp)
|
|
501
|
+
tail -r "$session_file" > "$tmp_rev" 2>/dev/null \
|
|
502
|
+
|| tac "$session_file" > "$tmp_rev" 2>/dev/null \
|
|
503
|
+
|| cp "$session_file" "$tmp_rev"
|
|
504
|
+
|
|
505
|
+
while IFS= read -r line; do
|
|
506
|
+
local op; op=$(echo "$line" | grep -o '"e":"[^"]*"' | cut -d'"' -f4)
|
|
507
|
+
if [[ "$op" == "MOVE" || "$op" == "DUPLICATE" ]]; then
|
|
508
|
+
local src dst fn
|
|
509
|
+
src=$(echo "$line" | grep -o '"src":"[^"]*"' | cut -d'"' -f4)
|
|
510
|
+
dst=$(echo "$line" | grep -o '"dst":"[^"]*"' | cut -d'"' -f4)
|
|
511
|
+
fn=$(basename "$dst")
|
|
512
|
+
if [[ ! -f "$dst" ]]; then
|
|
513
|
+
printf " ${Y}skip${RESET} %s\n" "$fn"; ((failed++)); continue
|
|
514
|
+
fi
|
|
515
|
+
printf " ${G}restore${RESET} %s\n" "$fn"
|
|
516
|
+
if ! $DRY_RUN; then
|
|
517
|
+
mkdir -p "$(dirname "$src")"
|
|
518
|
+
if mv "$dst" "$src" 2>/dev/null; then ((restored++))
|
|
519
|
+
else printf " ${R}error${RESET} %s\n" "$fn"; ((failed++)); fi
|
|
520
|
+
else
|
|
521
|
+
((restored++))
|
|
522
|
+
fi
|
|
523
|
+
fi
|
|
524
|
+
done < "$tmp_rev"
|
|
525
|
+
rm -f "$tmp_rev"
|
|
526
|
+
|
|
527
|
+
! $DRY_RUN && echo "{\"e\":\"ROLLBACK\",\"ts\":\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\",\"restored\":$restored,\"failed\":$failed}" >> "$session_file"
|
|
528
|
+
|
|
529
|
+
echo ""
|
|
530
|
+
echo -e " ${G}${BOLD}$restored${RESET} restored ${R}${BOLD}$failed${RESET} failed"
|
|
531
|
+
$DRY_RUN && echo -e "\n${Y} Dry run โ run without --dry-run to apply.${RESET}"
|
|
532
|
+
echo ""
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
# โโ Status โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
536
|
+
run_status() {
|
|
537
|
+
echo ""
|
|
538
|
+
local sessions; sessions=$(ls -t "$SESSIONS_DIR"/*.jsonl 2>/dev/null | head -10)
|
|
539
|
+
[[ -z "$sessions" ]] && echo -e " ${DIM}No sessions yet.${RESET}" && echo "" && return
|
|
540
|
+
|
|
541
|
+
local latest_file; latest_file=$(cat "$LATEST_LINK" 2>/dev/null)
|
|
542
|
+
printf " ${BOLD}%-9s %-19s %-11s %5s %5s %4s${RESET}\n" \
|
|
543
|
+
"Session" "Timestamp" "Status" "Moved" "Dupes" "Errs"
|
|
544
|
+
printf " %s\n" "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
|
|
545
|
+
|
|
546
|
+
for f in $sessions; do
|
|
547
|
+
local sid timestamp moved dupes errors end_line rolled marker=""
|
|
548
|
+
sid=$(basename "$f" .jsonl)
|
|
549
|
+
timestamp=$(head -1 "$f" | grep -o '"ts":"[^"]*"' | cut -d'"' -f4 | cut -c1-19)
|
|
550
|
+
end_line=$(grep '"e":"MOVE_DONE"' "$f" 2>/dev/null | tail -1)
|
|
551
|
+
moved=$(echo "$end_line" | grep -o '"moved":"[^"]*"' | cut -d'"' -f4)
|
|
552
|
+
dupes=$(echo "$end_line" | grep -o '"duplicates":"[^"]*"' | cut -d'"' -f4)
|
|
553
|
+
errors=$(echo "$end_line" | grep -o '"errors":"[^"]*"' | cut -d'"' -f4)
|
|
554
|
+
rolled=$(grep -c '"e":"ROLLBACK"' "$f" 2>/dev/null || echo 0)
|
|
555
|
+
|
|
556
|
+
local status_label
|
|
557
|
+
if [[ "$rolled" -gt 0 ]]; then status_label="${DIM}rolled back${RESET}"
|
|
558
|
+
elif [[ -n "$end_line" ]]; then status_label="${G}complete${RESET} "
|
|
559
|
+
else status_label="${Y}incomplete${RESET}"; fi
|
|
560
|
+
|
|
561
|
+
[[ "$f" == "$latest_file" ]] && marker=" ${C}โ latest${RESET}"
|
|
562
|
+
printf " ${C}%-9s${RESET} %-19s " "$sid" "$timestamp"
|
|
563
|
+
echo -ne "$status_label"
|
|
564
|
+
printf " %5s %5s %4s" "${moved:-0}" "${dupes:-0}" "${errors:-0}"
|
|
565
|
+
echo -e "$marker"
|
|
566
|
+
done
|
|
567
|
+
echo ""
|
|
568
|
+
echo -e " ${DIM}filo inspect <session-id> for details${RESET}"
|
|
569
|
+
echo ""
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
# โโ Main โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
573
|
+
case "$MODE" in
|
|
574
|
+
rollback) run_rollback ;;
|
|
575
|
+
status) run_status ;;
|
|
576
|
+
inspect)
|
|
577
|
+
INSPECT_SESSION="$1"
|
|
578
|
+
bash "$FILO_DIR/lib/inspect.sh" "$INSPECT_SESSION" "$VIEW" "$SESSIONS_DIR"
|
|
579
|
+
;;
|
|
580
|
+
organize)
|
|
581
|
+
SESSION_ID=$(date '+%s' | md5 | cut -c1-7)
|
|
582
|
+
SESSION_FILE="$SESSIONS_DIR/$SESSION_ID.jsonl"
|
|
583
|
+
echo "$SESSION_FILE" > "$LATEST_LINK"
|
|
584
|
+
rm -f "$COUNTS_FILE" "$EVENTS_FILE"
|
|
585
|
+
emit "SESSION_START" session="$SESSION_ID" dry_run="$DRY_RUN" view="$VIEW"
|
|
586
|
+
|
|
587
|
+
manifest="$LOG_DIR/manifest_$SESSION_ID.tsv"
|
|
588
|
+
|
|
589
|
+
VIEW_SCRIPT="$VIEWS_DIR/${VIEW}.sh"
|
|
590
|
+
[[ ! -f "$VIEW_SCRIPT" ]] && VIEW_SCRIPT="$VIEWS_DIR/standard.sh"
|
|
591
|
+
|
|
592
|
+
# Execution phases
|
|
593
|
+
run_scan "$manifest"
|
|
594
|
+
run_move "$manifest"
|
|
595
|
+
run_verify
|
|
596
|
+
|
|
597
|
+
# Read tracking checkpoints
|
|
598
|
+
local_counts=$(cat "$COUNTS_FILE" 2>/dev/null)
|
|
599
|
+
scan_line=$(echo "$local_counts" | sed -n '1p')
|
|
600
|
+
move_line=$(echo "$local_counts" | sed -n '2p')
|
|
601
|
+
verify_line=$(echo "$local_counts" | sed -n '3p')
|
|
602
|
+
|
|
603
|
+
scan_count=$(echo "$scan_line" | cut -d' ' -f1)
|
|
604
|
+
total_bytes=$(echo "$scan_line" | cut -d' ' -f2)
|
|
605
|
+
moved=$(echo "$move_line" | cut -d' ' -f1)
|
|
606
|
+
duplicates=$(echo "$move_line" | cut -d' ' -f2)
|
|
607
|
+
errors=$(echo "$move_line" | cut -d' ' -f3)
|
|
608
|
+
bytes_moved=$(echo "$move_line" | cut -d' ' -f4)
|
|
609
|
+
move_elapsed=$(echo "$move_line"| cut -d' ' -f5)
|
|
610
|
+
verified=$(echo "$verify_line" | cut -d' ' -f1)
|
|
611
|
+
mismatches=$(echo "$verify_line"| cut -d' ' -f2)
|
|
612
|
+
|
|
613
|
+
emit "SESSION_END" \
|
|
614
|
+
scan="${scan_count:-0}" moved="${moved:-0}" duplicates="${duplicates:-0}" \
|
|
615
|
+
errors="${errors:-0}" verified="${verified:-0}" mismatches="${mismatches:-0}" \
|
|
616
|
+
bytes_moved="${bytes_moved:-0}" elapsed_ms="${move_elapsed:-0}"
|
|
617
|
+
|
|
618
|
+
# Hand off execution stream to UI script engines
|
|
619
|
+
bash "$VIEW_SCRIPT" \
|
|
620
|
+
"$SESSION_ID" "$DRY_RUN" \
|
|
621
|
+
"${scan_count:-0}" "${moved:-0}" "${duplicates:-0}" "${errors:-0}" \
|
|
622
|
+
"${verified:-0}" "${mismatches:-0}" \
|
|
623
|
+
"${bytes_moved:-0}" "${move_elapsed:-0}" "${total_bytes:-0}" \
|
|
624
|
+
"$SESSION_FILE" "$HOME"
|
|
625
|
+
;;
|
|
626
|
+
esac
|
package/lib/inspect.sh
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# โโ filo inspect <session-id> [--view <view>] โโโโโโโโโโโโโ
|
|
3
|
+
# Reads a past session log and renders it in any view
|
|
4
|
+
|
|
5
|
+
SESSION_ARG=$1
|
|
6
|
+
VIEW=${2:-standard}
|
|
7
|
+
SESSIONS_DIR=$3
|
|
8
|
+
|
|
9
|
+
G='\033[0;32m' R='\033[0;31m' Y='\033[1;33m'
|
|
10
|
+
B='\033[0;34m' C='\033[0;36m' M='\033[0;35m'
|
|
11
|
+
BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m'
|
|
12
|
+
|
|
13
|
+
FILO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
14
|
+
VIEWS_DIR="$FILO_DIR/lib/views"
|
|
15
|
+
|
|
16
|
+
# Find session file
|
|
17
|
+
if [[ -z "$SESSION_ARG" ]]; then
|
|
18
|
+
# No session given โ show picker
|
|
19
|
+
echo ""
|
|
20
|
+
echo -e " ${BOLD}filo inspect${RESET} โ available sessions:"
|
|
21
|
+
echo ""
|
|
22
|
+
sessions=$(ls -t "$SESSIONS_DIR"/*.jsonl 2>/dev/null | head -10)
|
|
23
|
+
if [[ -z "$sessions" ]]; then
|
|
24
|
+
echo -e " ${DIM}No sessions found.${RESET}"; echo ""; exit 0
|
|
25
|
+
fi
|
|
26
|
+
i=1
|
|
27
|
+
for f in $sessions; do
|
|
28
|
+
sid=$(basename "$f" .jsonl)
|
|
29
|
+
ts=$(head -1 "$f" | grep -o '"ts":"[^"]*"' | cut -d'"' -f4 | cut -c1-19)
|
|
30
|
+
moved=$(grep '"e":"MOVE_DONE"' "$f" 2>/dev/null | grep -o '"moved":"[^"]*"' | cut -d'"' -f4)
|
|
31
|
+
errors=$(grep '"e":"MOVE_DONE"' "$f" 2>/dev/null | grep -o '"errors":"[^"]*"' | cut -d'"' -f4)
|
|
32
|
+
rolled=$(grep -c '"e":"ROLLBACK"' "$f" 2>/dev/null); rolled=${rolled:-0}
|
|
33
|
+
status="${G}ok${RESET}"
|
|
34
|
+
[[ "${errors:-0}" -gt 0 ]] && status="${R}errors${RESET}"
|
|
35
|
+
[[ $rolled -gt 0 ]] && status="${DIM}rolled back${RESET}"
|
|
36
|
+
echo -e " ${C}${sid}${RESET} $(printf '%-19s' "$ts") ${moved:-0} moved $status"
|
|
37
|
+
((i++))
|
|
38
|
+
done
|
|
39
|
+
echo ""
|
|
40
|
+
echo -e " Usage: ${C}filo inspect <session-id> [--view debug|transfer|compact]${RESET}"
|
|
41
|
+
echo ""
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Find the file
|
|
46
|
+
SESSION_FILE=$(ls "$SESSIONS_DIR/${SESSION_ARG}"*.jsonl 2>/dev/null | head -1)
|
|
47
|
+
if [[ ! -f "$SESSION_FILE" ]]; then
|
|
48
|
+
echo -e "\n ${R}Session not found: $SESSION_ARG${RESET}"
|
|
49
|
+
echo -e " ${DIM}Run 'filo inspect' to list available sessions.${RESET}\n"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
SESSION_ID=$(basename "$SESSION_FILE" .jsonl)
|
|
54
|
+
|
|
55
|
+
# Read summary from session
|
|
56
|
+
end_line=$(grep '"e":"MOVE_DONE"' "$SESSION_FILE" 2>/dev/null | tail -1)
|
|
57
|
+
verify_line=$(grep '"e":"VERIFY_DONE"' "$SESSION_FILE" 2>/dev/null | tail -1)
|
|
58
|
+
scan_line=$(grep '"e":"SCAN_DONE"' "$SESSION_FILE" 2>/dev/null | tail -1)
|
|
59
|
+
|
|
60
|
+
scan_count=$(echo "$scan_line" | grep -o '"count":"[^"]*"' | cut -d'"' -f4)
|
|
61
|
+
total_bytes=$(echo "$scan_line" | grep -o '"total_bytes":"[^"]*"' | cut -d'"' -f4)
|
|
62
|
+
moved=$(echo "$end_line" | grep -o '"moved":"[^"]*"' | cut -d'"' -f4)
|
|
63
|
+
duplicates=$(echo "$end_line" | grep -o '"duplicates":"[^"]*"' | cut -d'"' -f4)
|
|
64
|
+
errors=$(echo "$end_line" | grep -o '"errors":"[^"]*"' | cut -d'"' -f4)
|
|
65
|
+
bytes_moved=$(echo "$end_line" | grep -o '"bytes_moved":"[^"]*"' | cut -d'"' -f4)
|
|
66
|
+
elapsed=$(echo "$end_line" | grep -o '"elapsed_ms":"[^"]*"' | cut -d'"' -f4)
|
|
67
|
+
verified=$(echo "$verify_line" | grep -o '"verified":"[^"]*"' | cut -d'"' -f4)
|
|
68
|
+
mismatches=$(echo "$verify_line"| grep -o '"mismatches":"[^"]*"' | cut -d'"' -f4)
|
|
69
|
+
dry_run=$(head -1 "$SESSION_FILE" | grep -o '"dry_run":"[^"]*"' | cut -d'"' -f4)
|
|
70
|
+
dest_base="$HOME/Folder Manager"
|
|
71
|
+
|
|
72
|
+
# Check rollback status
|
|
73
|
+
rolled=$(grep -c '"e":"ROLLBACK"' "$SESSION_FILE" 2>/dev/null || echo 0)
|
|
74
|
+
[[ "$rolled" -gt 0 ]] && echo -e "\n ${Y}โ This session was rolled back.${RESET}"
|
|
75
|
+
|
|
76
|
+
# Render view
|
|
77
|
+
VIEW_SCRIPT="$VIEWS_DIR/${VIEW}.sh"
|
|
78
|
+
[[ ! -f "$VIEW_SCRIPT" ]] && VIEW_SCRIPT="$VIEWS_DIR/standard.sh"
|
|
79
|
+
|
|
80
|
+
bash "$VIEW_SCRIPT" \
|
|
81
|
+
"$SESSION_ID" "$dry_run" \
|
|
82
|
+
"${scan_count:-0}" "${moved:-0}" "${duplicates:-0}" "${errors:-0}" \
|
|
83
|
+
"${verified:-0}" "${mismatches:-0}" \
|
|
84
|
+
"${bytes_moved:-0}" "${elapsed:-0}" "${total_bytes:-0}" \
|
|
85
|
+
"$SESSION_FILE" "$dest_base"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# โโ Compact View โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3
|
+
# One line. Everything you need.
|
|
4
|
+
|
|
5
|
+
SESSION_ID=$1 DRY_RUN=$2
|
|
6
|
+
SCAN=$3 MOVED=$4 DUPES=$5 ERRORS=$6
|
|
7
|
+
VERIFIED=$7 MISMATCHES=$8
|
|
8
|
+
BYTES_MOVED=$9 ELAPSED=${10} TOTAL_BYTES=${11}
|
|
9
|
+
SESSION_FILE=${12} DEST_BASE=${13}
|
|
10
|
+
|
|
11
|
+
G='\033[0;32m' R='\033[0;31m' Y='\033[1;33m'
|
|
12
|
+
C='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m'
|
|
13
|
+
|
|
14
|
+
fmt_bytes() {
|
|
15
|
+
local b=$1
|
|
16
|
+
if [[ $b -ge 1073741824 ]]; then printf "%.1fGB" "$(echo "$b" | awk '{printf "%.1f", $1/1073741824}')";
|
|
17
|
+
elif [[ $b -ge 1048576 ]]; then printf "%.1fMB" "$(echo "$b" | awk '{printf "%.1f", $1/1048576}')";
|
|
18
|
+
elif [[ $b -ge 1024 ]]; then printf "%.1fKB" "$(echo "$b" | awk '{printf "%.1f", $1/1024}')";
|
|
19
|
+
else printf "%dB" "$b"; fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
echo ""
|
|
23
|
+
if [[ $ERRORS -eq 0 && $MISMATCHES -eq 0 ]]; then
|
|
24
|
+
STATUS="${G}โ${RESET}"
|
|
25
|
+
else
|
|
26
|
+
STATUS="${R}โ${RESET}"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
SIZE_STR=""
|
|
30
|
+
[[ $BYTES_MOVED -gt 0 ]] && SIZE_STR=" ยท $(fmt_bytes "$BYTES_MOVED")"
|
|
31
|
+
|
|
32
|
+
[[ "$DRY_RUN" == "true" ]] && DRY=" ${Y}dry-run${RESET}" || DRY=""
|
|
33
|
+
|
|
34
|
+
printf " $STATUS ${BOLD}%d${RESET} moved ${Y}%d${RESET} dupes ${R}%d${RESET} errors%s ${DIM}[%s]${RESET}%s\n" \
|
|
35
|
+
"$MOVED" "$DUPES" "$ERRORS" "$SIZE_STR" "$SESSION_ID" "$DRY"
|
|
36
|
+
|
|
37
|
+
[[ $ERRORS -gt 0 || $MISMATCHES -gt 0 ]] && \
|
|
38
|
+
printf " ${DIM}โ filo inspect %s --view debug${RESET}\n" "$SESSION_ID"
|
|
39
|
+
|
|
40
|
+
echo ""
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# โโ Debug View โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3
|
+
# Full diagnostic โ every file, checksum pairs, errors, warnings
|
|
4
|
+
|
|
5
|
+
SESSION_ID=$1 DRY_RUN=$2
|
|
6
|
+
SCAN=$3 MOVED=$4 DUPES=$5 ERRORS=$6
|
|
7
|
+
VERIFIED=$7 MISMATCHES=$8
|
|
8
|
+
BYTES_MOVED=$9 ELAPSED=${10} TOTAL_BYTES=${11}
|
|
9
|
+
SESSION_FILE=${12} DEST_BASE=${13}
|
|
10
|
+
|
|
11
|
+
G='\033[0;32m' R='\033[0;31m' Y='\033[1;33m'
|
|
12
|
+
B='\033[0;34m' C='\033[0;36m' M='\033[0;35m'
|
|
13
|
+
BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m'
|
|
14
|
+
|
|
15
|
+
shorten() { echo "${1/#$HOME/~}"; }
|
|
16
|
+
|
|
17
|
+
fmt_bytes() {
|
|
18
|
+
local b=$1
|
|
19
|
+
if [[ $b -ge 1048576 ]]; then printf "%.1f MB" "$(echo "$b" | awk '{printf "%.1f", $1/1048576}')";
|
|
20
|
+
elif [[ $b -ge 1024 ]]; then printf "%.1f KB" "$(echo "$b" | awk '{printf "%.1f", $1/1024}')";
|
|
21
|
+
else printf "%d B" "$b"; fi
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
echo ""
|
|
25
|
+
echo -e "${BOLD}${M} ๐ filo${RESET} ${DIM}debug view ยท session $SESSION_ID${RESET}"
|
|
26
|
+
[[ "$DRY_RUN" == "true" ]] && echo -e "${Y} dry run โ no files moved${RESET}"
|
|
27
|
+
echo ""
|
|
28
|
+
|
|
29
|
+
if [[ ! -f "$SESSION_FILE" ]]; then
|
|
30
|
+
echo -e "${R} Session file not found: $SESSION_FILE${RESET}"; exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# โโ Event log โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
34
|
+
echo -e " ${BOLD}Event Log${RESET}"
|
|
35
|
+
echo -e " ${DIM}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${RESET}"
|
|
36
|
+
|
|
37
|
+
while IFS= read -r line; do
|
|
38
|
+
ev=$(echo "$line" | grep -o '"e":"[^"]*"' | cut -d'"' -f4)
|
|
39
|
+
ts=$(echo "$line" | grep -o '"ts":"[^"]*"' | cut -d'"' -f4 | cut -c12-19)
|
|
40
|
+
|
|
41
|
+
case "$ev" in
|
|
42
|
+
PHASE)
|
|
43
|
+
name=$(echo "$line" | grep -o '"name":"[^"]*"' | cut -d'"' -f4 | tr '[:lower:]' '[:upper:]')
|
|
44
|
+
echo -e "\n ${BOLD}${B}โโ $name โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${RESET}"
|
|
45
|
+
;;
|
|
46
|
+
|
|
47
|
+
MOVE)
|
|
48
|
+
src=$(echo "$line" | grep -o '"src":"[^"]*"' | cut -d'"' -f4)
|
|
49
|
+
dst=$(echo "$line" | grep -o '"dst":"[^"]*"' | cut -d'"' -f4)
|
|
50
|
+
sc=$(echo "$line" | grep -o '"src_checksum":"[^"]*"'| cut -d'"' -f4 | cut -c1-8)
|
|
51
|
+
dc=$(echo "$line" | grep -o '"dst_checksum":"[^"]*"'| cut -d'"' -f4 | cut -c1-8)
|
|
52
|
+
st=$(echo "$line" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
|
|
53
|
+
sz=$(echo "$line" | grep -o '"size":"[^"]*"' | cut -d'"' -f4)
|
|
54
|
+
el=$(echo "$line" | grep -o '"elapsed_ms":"[^"]*"' | cut -d'"' -f4)
|
|
55
|
+
fn=$(basename "$src")
|
|
56
|
+
|
|
57
|
+
if [[ "$st" == "ok" || "$st" == "dry_run" ]]; then
|
|
58
|
+
printf " ${G}[MOVE]${RESET} ${BOLD}%-35s${RESET}\n" "$fn"
|
|
59
|
+
else
|
|
60
|
+
printf " ${Y}[MOVE]${RESET} ${BOLD}%-35s${RESET} ${R}โ $st${RESET}\n" "$fn"
|
|
61
|
+
fi
|
|
62
|
+
printf " ${DIM} src: %-45s cs: %s${RESET}\n" "$(shorten "$src")" "$sc"
|
|
63
|
+
printf " ${DIM} dst: %-45s cs: %s${RESET}\n" "$(shorten "$dst")" "$dc"
|
|
64
|
+
[[ -n "$sz" && -n "$el" ]] && printf " ${DIM} size: %-10s time: %sms${RESET}\n" "$(fmt_bytes "$sz")" "$el"
|
|
65
|
+
if [[ "$sc" != "$dc" && -n "$sc" && -n "$dc" ]]; then
|
|
66
|
+
echo -e " ${R} โ checksum mismatch src=$sc dst=$dc${RESET}"
|
|
67
|
+
fi
|
|
68
|
+
;;
|
|
69
|
+
|
|
70
|
+
DUPLICATE)
|
|
71
|
+
src=$(echo "$line" | grep -o '"src":"[^"]*"' | cut -d'"' -f4)
|
|
72
|
+
dst=$(echo "$line" | grep -o '"dst":"[^"]*"' | cut -d'"' -f4)
|
|
73
|
+
sc=$(echo "$line" | grep -o '"src_checksum":"[^"]*"'| cut -d'"' -f4 | cut -c1-8)
|
|
74
|
+
dc=$(echo "$line" | grep -o '"dst_checksum":"[^"]*"'| cut -d'"' -f4 | cut -c1-8)
|
|
75
|
+
fn=$(basename "$src")
|
|
76
|
+
printf " ${Y}[DUPE]${RESET} ${BOLD}%-35s${RESET} โ Duplicates/\n" "$fn"
|
|
77
|
+
printf " ${DIM} src: $(shorten "$src")${RESET}\n"
|
|
78
|
+
printf " ${DIM} dst: $(shorten "$dst")${RESET}\n"
|
|
79
|
+
[[ "$sc" != "$dc" && -n "$sc" && -n "$dc" ]] \
|
|
80
|
+
&& echo -e " ${R} โ checksum mismatch src=$sc dst=$dc${RESET}"
|
|
81
|
+
;;
|
|
82
|
+
|
|
83
|
+
ERROR)
|
|
84
|
+
src=$(echo "$line" | grep -o '"src":"[^"]*"' | cut -d'"' -f4)
|
|
85
|
+
reason=$(echo "$line" | grep -o '"reason":"[^"]*"' | cut -d'"' -f4)
|
|
86
|
+
fn=$(basename "$src")
|
|
87
|
+
printf " ${R}[ERROR]${RESET} ${BOLD}%-35s${RESET}\n" "$fn"
|
|
88
|
+
printf " ${DIM} src: $(shorten "$src")${RESET}\n"
|
|
89
|
+
printf " ${R} reason: $reason${RESET}\n"
|
|
90
|
+
;;
|
|
91
|
+
|
|
92
|
+
VERIFY_FAIL)
|
|
93
|
+
dst=$(echo "$line" | grep -o '"dst":"[^"]*"' | cut -d'"' -f4)
|
|
94
|
+
exp=$(echo "$line" | grep -o '"expected":"[^"]*"' | cut -d'"' -f4 | cut -c1-8)
|
|
95
|
+
act=$(echo "$line" | grep -o '"actual":"[^"]*"' | cut -d'"' -f4 | cut -c1-8)
|
|
96
|
+
fn=$(basename "$dst")
|
|
97
|
+
printf " ${R}[VERIFY]${RESET} ${BOLD}%-35s${RESET} ${R}โ checksum mismatch${RESET}\n" "$fn"
|
|
98
|
+
printf " ${DIM} expected: %s actual: %s${RESET}\n" "$exp" "$act"
|
|
99
|
+
printf " ${DIM} dst: $(shorten "$dst")${RESET}\n"
|
|
100
|
+
;;
|
|
101
|
+
|
|
102
|
+
VERIFY_MISSING)
|
|
103
|
+
dst=$(echo "$line" | grep -o '"dst":"[^"]*"' | cut -d'"' -f4)
|
|
104
|
+
fn=$(basename "$dst")
|
|
105
|
+
printf " ${R}[MISSING]${RESET} ${BOLD}%-35s${RESET} ${R}โ not found at destination${RESET}\n" "$fn"
|
|
106
|
+
;;
|
|
107
|
+
|
|
108
|
+
ALGO)
|
|
109
|
+
strategy=$(echo "$line" | grep -o '"strategy":"[^"]*"' | cut -d'"' -f4)
|
|
110
|
+
ext_ms=$(echo "$line" | grep -o '"ext_ms":"[^"]*"' | cut -d'"' -f4)
|
|
111
|
+
pat_ms=$(echo "$line" | grep -o '"pattern_ms":"[^"]*"'| cut -d'"' -f4)
|
|
112
|
+
mime_ms=$(echo "$line" | grep -o '"mime_ms":"[^"]*"' | cut -d'"' -f4)
|
|
113
|
+
echo -e "\n ${BOLD}${B}โโ ALGORITHM โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${RESET}"
|
|
114
|
+
printf " ${DIM} Extension Hash: %sms Name Pattern: %sms MIME: %sms${RESET}\n" \
|
|
115
|
+
"$ext_ms" "$pat_ms" "$mime_ms"
|
|
116
|
+
echo -e " ${G} Selected: $strategy${RESET}"
|
|
117
|
+
;;
|
|
118
|
+
|
|
119
|
+
SESSION_END)
|
|
120
|
+
echo ""
|
|
121
|
+
echo -e " ${BOLD}${B}โโ SUMMARY โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${RESET}"
|
|
122
|
+
sc2=$(echo "$line" | grep -o '"scan":"[^"]*"' | cut -d'"' -f4)
|
|
123
|
+
mv2=$(echo "$line" | grep -o '"moved":"[^"]*"' | cut -d'"' -f4)
|
|
124
|
+
du2=$(echo "$line" | grep -o '"duplicates":"[^"]*"' | cut -d'"' -f4)
|
|
125
|
+
er2=$(echo "$line" | grep -o '"errors":"[^"]*"' | cut -d'"' -f4)
|
|
126
|
+
vr2=$(echo "$line" | grep -o '"verified":"[^"]*"' | cut -d'"' -f4)
|
|
127
|
+
mm2=$(echo "$line" | grep -o '"mismatches":"[^"]*"' | cut -d'"' -f4)
|
|
128
|
+
total=$(( ${mv2:-0} + ${du2:-0} + ${er2:-0} ))
|
|
129
|
+
printf " %-24s %s / %s\n" "Accounted for:" "$total" "${sc2:-0}"
|
|
130
|
+
printf " ${G}%-24s %s${RESET}\n" "Moved:" "${mv2:-0}"
|
|
131
|
+
printf " ${Y}%-24s %s${RESET}\n" "Duplicates:" "${du2:-0}"
|
|
132
|
+
printf " ${R}%-24s %s${RESET}\n" "Errors:" "${er2:-0}"
|
|
133
|
+
printf " ${B}%-24s %s${RESET}\n" "Verified:" "${vr2:-0}"
|
|
134
|
+
[[ "${mm2:-0}" -gt 0 ]] && printf " ${R}%-24s %s${RESET}\n" "Checksum mismatches:" "${mm2:-0}"
|
|
135
|
+
;;
|
|
136
|
+
esac
|
|
137
|
+
done < "$SESSION_FILE"
|
|
138
|
+
|
|
139
|
+
echo ""
|
|
140
|
+
echo -e " ${DIM}session log: ~/.filo/sessions/${SESSION_ID}.jsonl${RESET}"
|
|
141
|
+
echo ""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# โโ Standard View โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3
|
+
# Clean, minimal. Just what happened and where.
|
|
4
|
+
|
|
5
|
+
SESSION_ID=$1 DRY_RUN=$2
|
|
6
|
+
SCAN=$3 MOVED=$4 DUPES=$5 ERRORS=$6
|
|
7
|
+
VERIFIED=$7 MISMATCHES=$8
|
|
8
|
+
BYTES_MOVED=$9 ELAPSED=${10} TOTAL_BYTES=${11}
|
|
9
|
+
SESSION_FILE=${12} DEST_BASE=${13}
|
|
10
|
+
|
|
11
|
+
G='\033[0;32m' R='\033[0;31m' Y='\033[1;33m'
|
|
12
|
+
B='\033[0;34m' C='\033[0;36m' M='\033[0;35m'
|
|
13
|
+
BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m'
|
|
14
|
+
|
|
15
|
+
fmt_bytes() {
|
|
16
|
+
local b=$1
|
|
17
|
+
if [[ $b -ge 1073741824 ]]; then printf "%.1f GB" "$(echo "$b" | awk '{printf "%.1f", $1/1073741824}')";
|
|
18
|
+
elif [[ $b -ge 1048576 ]]; then printf "%.1f MB" "$(echo "$b" | awk '{printf "%.1f", $1/1048576}')";
|
|
19
|
+
elif [[ $b -ge 1024 ]]; then printf "%.1f KB" "$(echo "$b" | awk '{printf "%.1f", $1/1024}')";
|
|
20
|
+
else printf "%d B" "$b"; fi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
echo ""
|
|
24
|
+
echo -e "${BOLD}${M} ๐ filo${RESET} ${DIM}session $SESSION_ID${RESET}"
|
|
25
|
+
[[ "$DRY_RUN" == "true" ]] && echo -e "${Y} dry run โ no files moved${RESET}"
|
|
26
|
+
echo ""
|
|
27
|
+
|
|
28
|
+
# Result line
|
|
29
|
+
if [[ $ERRORS -eq 0 ]]; then
|
|
30
|
+
echo -e " ${G}${BOLD}โ${RESET} ${BOLD}$MOVED${RESET} moved ${Y}${BOLD}$DUPES${RESET} duplicate ${G}${BOLD}$ERRORS${RESET} errors"
|
|
31
|
+
else
|
|
32
|
+
echo -e " ${R}${BOLD}โ${RESET} ${BOLD}$MOVED${RESET} moved ${Y}${BOLD}$DUPES${RESET} duplicate ${R}${BOLD}$ERRORS${RESET} errors"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo ""
|
|
36
|
+
|
|
37
|
+
# Source โ destination
|
|
38
|
+
echo -e " ${DIM}from${RESET} ~/Downloads, ~/Desktop"
|
|
39
|
+
echo -e " ${DIM}to${RESET} ~/Music ~/Movies ~/Pictures ~/Documents"
|
|
40
|
+
|
|
41
|
+
# Size
|
|
42
|
+
if [[ $BYTES_MOVED -gt 0 ]]; then
|
|
43
|
+
echo -e " ${DIM}size${RESET} $(fmt_bytes "$BYTES_MOVED")"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
echo ""
|
|
47
|
+
|
|
48
|
+
# Warnings
|
|
49
|
+
if [[ $MISMATCHES -gt 0 ]]; then
|
|
50
|
+
echo -e " ${R}โ $MISMATCHES checksum mismatch(es) โ run: filo inspect $SESSION_ID --view debug${RESET}"
|
|
51
|
+
fi
|
|
52
|
+
if [[ $ERRORS -gt 0 ]]; then
|
|
53
|
+
echo -e " ${R}โ $ERRORS error(s) โ run: filo inspect $SESSION_ID --view debug${RESET}"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Footer
|
|
57
|
+
echo -e " ${DIM}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${RESET}"
|
|
58
|
+
echo -e " ${DIM}undo: ${RESET}${C}filo rollback${RESET}"
|
|
59
|
+
echo -e " ${DIM}details: ${RESET}${C}filo inspect $SESSION_ID${RESET}"
|
|
60
|
+
echo ""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# โโ Transfer View โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
3
|
+
# File operation stats, throughput, algo analysis
|
|
4
|
+
|
|
5
|
+
SESSION_ID=$1 DRY_RUN=$2
|
|
6
|
+
SCAN=$3 MOVED=$4 DUPES=$5 ERRORS=$6
|
|
7
|
+
VERIFIED=$7 MISMATCHES=$8
|
|
8
|
+
BYTES_MOVED=$9 ELAPSED=${10} TOTAL_BYTES=${11}
|
|
9
|
+
SESSION_FILE=${12} DEST_BASE=${13}
|
|
10
|
+
|
|
11
|
+
G='\033[0;32m' R='\033[0;31m' Y='\033[1;33m'
|
|
12
|
+
B='\033[0;34m' C='\033[0;36m' M='\033[0;35m'
|
|
13
|
+
BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m'
|
|
14
|
+
|
|
15
|
+
fmt_bytes() {
|
|
16
|
+
local b=$1
|
|
17
|
+
if [[ $b -ge 1073741824 ]]; then printf "%.1f GB" "$(echo "$b" | awk '{printf "%.1f", $1/1073741824}')";
|
|
18
|
+
elif [[ $b -ge 1048576 ]]; then printf "%.1f MB" "$(echo "$b" | awk '{printf "%.1f", $1/1048576}')";
|
|
19
|
+
elif [[ $b -ge 1024 ]]; then printf "%.1f KB" "$(echo "$b" | awk '{printf "%.1f", $1/1024}')";
|
|
20
|
+
else printf "%d B" "$b"; fi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
bar() {
|
|
24
|
+
local val=$1 max=$2 width=${3:-20}
|
|
25
|
+
[[ $max -eq 0 ]] && max=1
|
|
26
|
+
local filled=$(( (val * width) / max ))
|
|
27
|
+
local empty=$(( width - filled ))
|
|
28
|
+
printf "${G}"
|
|
29
|
+
printf '%0.sโ' $(seq 1 $filled 2>/dev/null) 2>/dev/null
|
|
30
|
+
printf "${DIM}"
|
|
31
|
+
printf '%0.sโ' $(seq 1 $empty 2>/dev/null) 2>/dev/null
|
|
32
|
+
printf "${RESET}"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
echo ""
|
|
36
|
+
echo -e "${BOLD}${M} ๐ filo${RESET} ${DIM}transfer view ยท session $SESSION_ID${RESET}"
|
|
37
|
+
[[ "$DRY_RUN" == "true" ]] && echo -e "${Y} dry run${RESET}"
|
|
38
|
+
echo ""
|
|
39
|
+
|
|
40
|
+
# โโ Transfer stats โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
41
|
+
echo -e " ${BOLD}Transfer${RESET}"
|
|
42
|
+
echo -e " ${DIM}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${RESET}"
|
|
43
|
+
|
|
44
|
+
total=$((MOVED + DUPES + ERRORS))
|
|
45
|
+
[[ $total -eq 0 ]] && total=1
|
|
46
|
+
|
|
47
|
+
# Per-category breakdown from session file
|
|
48
|
+
if [[ -f "$SESSION_FILE" ]]; then
|
|
49
|
+
declare -A cat_counts cat_bytes
|
|
50
|
+
while IFS= read -r line; do
|
|
51
|
+
local_e=$(echo "$line" | grep -o '"e":"[^"]*"' | cut -d'"' -f4)
|
|
52
|
+
if [[ "$local_e" == "MOVE" || "$local_e" == "DUPLICATE" ]]; then
|
|
53
|
+
cat=$(echo "$line" | grep -o '"category":"[^"]*"' | cut -d'"' -f4)
|
|
54
|
+
sz=$(echo "$line" | grep -o '"size":"[^"]*"' | cut -d'"' -f4)
|
|
55
|
+
[[ -z "$cat" ]] && cat="Miscellaneous"
|
|
56
|
+
[[ -z "$sz" ]] && sz=0
|
|
57
|
+
cat_counts[$cat]=$(( ${cat_counts[$cat]:-0} + 1 ))
|
|
58
|
+
cat_bytes[$cat]=$(( ${cat_bytes[$cat]:-0} + sz ))
|
|
59
|
+
fi
|
|
60
|
+
done < "$SESSION_FILE"
|
|
61
|
+
|
|
62
|
+
for cat in "${!cat_counts[@]}"; do
|
|
63
|
+
local cnt=${cat_counts[$cat]}
|
|
64
|
+
local bytes=${cat_bytes[$cat]}
|
|
65
|
+
printf " ${C}%-16s${RESET} " "$cat"
|
|
66
|
+
bar "$cnt" "$total" 16
|
|
67
|
+
printf " %3d files %s\n" "$cnt" "$(fmt_bytes "$bytes")"
|
|
68
|
+
done
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
echo ""
|
|
72
|
+
|
|
73
|
+
# โโ Throughput โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
74
|
+
echo -e " ${BOLD}Throughput${RESET}"
|
|
75
|
+
echo -e " ${DIM}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${RESET}"
|
|
76
|
+
|
|
77
|
+
elapsed_s=0
|
|
78
|
+
[[ $ELAPSED -gt 0 ]] && elapsed_s=$(echo "$ELAPSED" | awk '{printf "%.1f", $1/1000}')
|
|
79
|
+
|
|
80
|
+
rate_bps=0
|
|
81
|
+
if [[ $ELAPSED -gt 0 && $BYTES_MOVED -gt 0 ]]; then
|
|
82
|
+
rate_bps=$(echo "$BYTES_MOVED $ELAPSED" | awk '{printf "%d", ($1 / $2) * 1000}')
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
printf " %-22s %s\n" "Total data moved:" "$(fmt_bytes "$BYTES_MOVED")"
|
|
86
|
+
printf " %-22s %ss\n" "Elapsed:" "$elapsed_s"
|
|
87
|
+
printf " %-22s %s/s\n" "Avg throughput:" "$(fmt_bytes "$rate_bps")"
|
|
88
|
+
printf " %-22s %d / %d\n" "Files (moved/scan):" "$MOVED" "$SCAN"
|
|
89
|
+
printf " %-22s %d\n" "Verified:" "$VERIFIED"
|
|
90
|
+
[[ $MISMATCHES -gt 0 ]] && printf " ${R}%-22s %d${RESET}\n" "Checksum mismatches:" "$MISMATCHES"
|
|
91
|
+
|
|
92
|
+
echo ""
|
|
93
|
+
|
|
94
|
+
# โโ Algo benchmark โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
95
|
+
echo -e " ${BOLD}Classification Algorithm${RESET}"
|
|
96
|
+
echo -e " ${DIM}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${RESET}"
|
|
97
|
+
|
|
98
|
+
algo_line=$(grep '"e":"ALGO"' "$SESSION_FILE" 2>/dev/null | tail -1)
|
|
99
|
+
if [[ -n "$algo_line" ]]; then
|
|
100
|
+
strategy=$(echo "$algo_line" | grep -o '"strategy":"[^"]*"' | cut -d'"' -f4)
|
|
101
|
+
ext_ms=$(echo "$algo_line" | grep -o '"ext_ms":"[^"]*"' | cut -d'"' -f4)
|
|
102
|
+
ext_acc=$(echo "$algo_line" | grep -o '"ext_acc":"[^"]*"' | cut -d'"' -f4)
|
|
103
|
+
pat_ms=$(echo "$algo_line" | grep -o '"pattern_ms":"[^"]*"' | cut -d'"' -f4)
|
|
104
|
+
pat_acc=$(echo "$algo_line" | grep -o '"pattern_acc":"[^"]*"' | cut -d'"' -f4)
|
|
105
|
+
mime_ms=$(echo "$algo_line" | grep -o '"mime_ms":"[^"]*"' | cut -d'"' -f4)
|
|
106
|
+
mime_acc=$(echo "$algo_line" | grep -o '"mime_acc":"[^"]*"' | cut -d'"' -f4)
|
|
107
|
+
n=$(echo "$algo_line" | grep -o '"n":"[^"]*"' | cut -d'"' -f4)
|
|
108
|
+
|
|
109
|
+
printf " ${BOLD}%-24s %-14s %-6s %s${RESET}\n" "Strategy" "Complexity" "Time" "Accuracy"
|
|
110
|
+
printf " ${DIM}%s${RESET}\n" "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
|
|
111
|
+
|
|
112
|
+
selected_marker() { [[ "$strategy" == "$1" ]] && echo -e " ${G}โ selected${RESET}" || echo ""; }
|
|
113
|
+
|
|
114
|
+
printf " ${G}%-24s${RESET} %-14s %4sms %3s%% %s\n" \
|
|
115
|
+
"Extension Hash" "O(1)" "$ext_ms" "$ext_acc" "$(selected_marker "Extension Hash")"
|
|
116
|
+
printf " ${C}%-24s${RESET} %-14s %4sms %3s%% %s\n" \
|
|
117
|
+
"Name Pattern" "O(n log n)" "$pat_ms" "$pat_acc" "$(selected_marker "Name Pattern")"
|
|
118
|
+
printf " ${Y}%-24s${RESET} %-14s %4sms %3s%% %s\n" \
|
|
119
|
+
"MIME Detection" "O(n)" "$mime_ms" "$mime_acc" "$(selected_marker "MIME Detection")"
|
|
120
|
+
|
|
121
|
+
echo -e " ${DIM}sample: $n files${RESET}"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
echo ""
|
|
125
|
+
echo -e " ${DIM}undo: filo rollback ยท details: filo inspect $SESSION_ID --view debug${RESET}"
|
|
126
|
+
echo ""
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filo-organizer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "file + logic โ intelligent file organizer for macOS with algorithm benchmarking, checksums, and git-style rollback",
|
|
5
|
+
"keywords": ["file-organizer", "folder-manager", "macos", "cli", "productivity", "automation"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"filo": "./bin/filo.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=14.0.0"
|
|
17
|
+
},
|
|
18
|
+
"os": [
|
|
19
|
+
"darwin"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/ST0RM-Z/filo.git"
|
|
24
|
+
}
|
|
25
|
+
}
|