1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
use clap::Parser;
use rtimelogger::config::Config;
use rtimelogger::{db, export};
use rusqlite::Connection;
mod commands;
use rtimelogger::cli::{Cli, Commands};
fn main() -> rusqlite::Result<()> {
let cli = Cli::parse();
// Ensure filesystem migration ran early (before any DB open). This moves old "%APPDATA%/rtimelog" or
// "$HOME/.rtimelog" to the new location and renames config/db references if needed.
if let Err(e) = rtimelogger::config::migrate::run_fs_migration() {
eprintln!("⚠️ Filesystem migration warning: {}", e);
}
// Ensure config dir exists so Connection::open can create the DB file inside it.
if let Err(e) = std::fs::create_dir_all(Config::config_dir()) {
eprintln!("❌ Failed to create config directory: {}", e);
return Err(rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(1),
Some(format!("Failed to create config dir: {}", e)),
));
}
// Determine DB path without loading the full config (Config::load may read files under
// $HOME or %APPDATA% which tests may control); prefer to avoid reading it when --test is set.
let db_path = if let Some(custom) = &cli.db {
let custom_path = std::path::Path::new(custom);
if custom_path.is_absolute() {
custom.to_string()
} else {
Config::config_dir()
.join(custom_path)
.to_string_lossy()
.to_string()
}
} else if cli.test {
// In test mode: use the default file name under the test config dir, but DO NOT call Config::load()
Config::config_dir()
.join("rtimelogger.sqlite")
.to_string_lossy()
.to_string()
} else {
// Production: load the configuration and use the database path from it
let config = Config::load();
config.database.clone()
};
// Now prepare a `config` object for use by commands; when running under --test or when --db is
// provided we construct a default config (matching Config::load() defaults) and point its
// `database` to the resolved db_path. Only when neither `--db` nor `--test` are used we call
// `Config::load()` to read possible overrides from disk.
let config = if cli.test || cli.db.is_some() {
Config {
database: db_path.clone(),
default_position: "O".to_string(),
min_work_duration: "8h".to_string(),
min_duration_lunch_break: 30,
max_duration_lunch_break: 90,
separator_char: "-".to_string(),
show_weekday: "None".to_string(),
}
} else {
// For production, we load the configuration from disk.
Config::load()
};
println!();
// Handle `init` separately because it may need to create config/db files first
if let Commands::Init = &cli.command {
return commands::handle_init(&cli, &db_path);
}
// For other commands, open a single shared connection, set useful PRAGMA and ensure DB is initialized (creates
// base tables and runs pending migrations).
// Try to open the DB; if opening fails (e.g. CannotOpen), attempt remediation once: run FS migration,
// create parent directories and try to touch the DB file, then retry.
let mut conn = match Connection::open(&db_path) {
Ok(c) => c,
Err(e) => {
eprintln!(
"⚠️ Failed to open DB at {:?}: {} -- attempting remediation",
db_path, e
);
// Diagnostic: print parent/exists/info to help debugging
let p = std::path::Path::new(&db_path);
if let Some(parent) = p.parent() {
eprintln!(" -> DB parent exists: {}", parent.exists());
if parent.exists() {
match std::fs::metadata(parent) {
Ok(md) => eprintln!(
" parent metadata: is_dir={} readonly={}",
md.is_dir(),
md.permissions().readonly()
),
Err(me) => eprintln!(" parent metadata error: {}", me),
}
}
}
eprintln!(
" -> DB file exists: {}",
std::path::Path::new(&db_path).exists()
);
// Re-run filesystem migration (best-effort)
if let Err(e2) = rtimelogger::config::migrate::run_fs_migration() {
eprintln!("⚠️ Filesystem migration (retry) warning: {}", e2);
}
// Ensure parent dir exists
if let Some(parent) = std::path::Path::new(&db_path).parent()
&& let Err(e3) = std::fs::create_dir_all(parent)
{
eprintln!(
"❌ Failed to create parent directory for DB {:?}: {}",
parent, e3
);
}
// Try to create (touch) the DB file so sqlite can open it
if let Err(e4) = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&db_path)
{
eprintln!("⚠️ Could not create DB file {:?}: {}", db_path, e4);
}
// Retry opening once
match Connection::open(&db_path) {
Ok(c2) => c2,
Err(e_final) => {
eprintln!("❌ Final attempt to open DB failed: {}", e_final);
// Extra diagnostic: list config dir contents
if let Some(parent) = std::path::Path::new(&db_path).parent() {
match std::fs::read_dir(parent) {
Ok(rd) => {
let names: Vec<String> = rd
.filter_map(|r| {
r.ok().and_then(|e| e.file_name().into_string().ok())
})
.collect();
eprintln!(" -> Contents of {:?}: {:?}", parent, names);
}
Err(re) => {
eprintln!(" -> Could not read parent dir {:?}: {}", parent, re)
}
}
}
return Err(e_final);
}
}
}
};
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "foreign_keys", "ON")?;
db::init_db(&conn)?;
match &cli.command {
Commands::Add { .. } => commands::handle_add(&cli.command, &mut conn, &config)?,
Commands::Del { .. } => commands::handle_del(&cli.command, &mut conn)?,
Commands::List {
period,
pos,
now,
details,
events,
pairs,
summary,
} => {
let args = commands::HandleListArgs {
period: period.clone(),
pos: pos.clone(),
now: *now,
details: *details,
events: *events,
pairs: *pairs,
summary: *summary,
};
commands::handle_list(&args, &conn, &config)?
}
Commands::Conf { .. } => commands::handle_conf(&cli.command)?,
Commands::Log { .. } => commands::handle_log(&cli.command, &conn)?,
Commands::Init => {
// Already handled, but included for exhaustiveness
}
Commands::Backup { file, compress } => {
if let Err(e) = commands::handle_backup(&config, file, compress) {
eprintln!("❌ Backup failed: {}", e);
}
}
Commands::Export { .. } => {
if let Err(e) = export::handle_export(&cli.command, &conn) {
eprintln!("❌ Export failed: {}", e);
};
}
}
Ok(())
}