Skip to content
This repository was archived by the owner on Nov 6, 2025. It is now read-only.

Commit d326a67

Browse files
Impl. user config and asst. command fixes (#34)
1 parent deca33e commit d326a67

File tree

7 files changed

+176
-70
lines changed

7 files changed

+176
-70
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include bak/default.cfg

bak/__main__.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import os
22
from datetime import datetime
3+
from shutil import copy2
34

45
import click
56
from click_default_group import DefaultGroup
7+
import config
68

7-
from . import commands
9+
from bak import commands
810

911

1012
def __print_help():
@@ -43,13 +45,20 @@ def bak_up(filename):
4345

4446

4547
@bak.command("down", help="Restore from a .bakfile (.bakfiles deleted without '--keep')")
46-
@click.option("--keep", "-k", is_flag=True, default=False, help="Keep .bakfiles")
48+
@click.option("--keep", "-k",
49+
is_flag=True,
50+
default=False,
51+
help="Keep .bakfiles")
52+
@click.option("--quietly", "-q",
53+
is_flag=True,
54+
default=False,
55+
help="No confirmation prompt")
4756
@click.argument("filename", required=True, type=click.Path(exists=True))
48-
def bak_down(filename, keep):
57+
def bak_down(filename, keep, quietly):
4958
if not filename:
5059
click.echo("A filename or operation is required.\n"
5160
"\tbak --help")
52-
commands.bak_down_cmd(filename, keep)
61+
commands.bak_down_cmd(filename, keep, quietly)
5362

5463

5564
@bak.command("off", help="Use when finished to delete .bakfiles")

bak/commands/__init__.py

Lines changed: 125 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from warnings import warn
1010

1111
import click
12+
from config import Config
1213

1314
from bak.data import bakfile, bak_db
1415

@@ -22,8 +23,14 @@
2223
config_dir = os.environ["XDG_CONFIG_HOME"]
2324
except KeyError:
2425
config_dir = os.path.expanduser("~/.config")
25-
bak_dir = os.path.join(data_dir, "bak", "bakfiles")
26-
bak_db_loc = os.path.join(data_dir, "bak", "bak.db")
26+
27+
config_file = os.path.join(config_dir, 'bak.cfg')
28+
cfg = Config(config_file)
29+
30+
bak_dir = cfg['bakfile_location'] or os.path.join(data_dir,
31+
"bak", "bakfiles")
32+
bak_db_loc = cfg['bak_database_location'] or \
33+
os.path.join(data_dir, "bak", "bak.db")
2734

2835
if not os.path.exists(bak_dir):
2936
os.makedirs(bak_dir)
@@ -38,14 +45,14 @@ def expandpath(i_path):
3845
def _assemble_bakfile(filename):
3946
time_now = datetime.now()
4047
splitname = os.path.split(expandpath(filename))
41-
bakfile_name = "".join([".".join(i[1:].replace("/", "-") for i in splitname[:-1]) +
48+
bakfile_name = "".join([".".join(i[1:].replace("/", "-")
49+
for i in splitname[:-1]) +
4250
'-' +
43-
splitname[-1], # [os.path.split(filename)[-1],
51+
splitname[-1],
4452
".",
4553
'-'.join(str(
4654
time_now.timestamp()).split('.')),
4755
".bak"]).replace(" ", "-")
48-
# TODO #26 get bakfile directory from config
4956
bakfile_path = os.path.join(bak_dir, bakfile_name)
5057

5158
new_bak_entry = bakfile.BakFile(os.path.basename(filename),
@@ -56,53 +63,71 @@ def _assemble_bakfile(filename):
5663
return new_bak_entry
5764

5865

59-
default_select_prompt = ("Enter a number, or: (V)iew (C)ancel", 'c')
66+
default_select_prompt = ("Enter a number, or: (V)iew (D)iff (C)ancel", 'c')
6067

6168

62-
def _get_bakfile_entry(filename, select_prompt=default_select_prompt, err=False):
69+
def _get_bakfile_entry(filename,
70+
select_prompt=default_select_prompt,
71+
err=False):
6372
entries = db_handler.get_bakfile_entries(expandpath(filename))
6473
if not entries or len(entries) == 0:
6574
return None
66-
return entries[0] if len(entries) == 1 else _do_select_bakfile(entries, select_prompt, err)
75+
# If there's only one bakfile corresponding to filename, return that.
76+
# If there's more than one, disambiguate.
77+
return entries[0] if len(entries) == 1 else \
78+
_do_select_bakfile(entries, select_prompt, err)
6779

6880

6981
def _do_select_bakfile(bakfiles: List[bakfile.BakFile],
7082
select_prompt=default_select_prompt,
7183
err=False):
7284
click.echo(
73-
f"Found {len(bakfiles)} bakfiles for file: {bakfiles[0].orig_abspath}", err=err)
85+
f"Found {len(bakfiles)} bakfiles for file: {bakfiles[0].orig_abspath}",
86+
err=err)
7487
click.echo("Please select from the following: ", err=err)
7588
_range = range(len(bakfiles))
7689
for i in _range:
7790
click.echo(
78-
f"{i + 1}: .bakfile last modified at {bakfiles[i].date_modified}", err=err)
79-
choice = click.prompt(*select_prompt, err=err)
80-
# TODO add diff and print as choices here
81-
# "Enter a number, or: (V)iew (C)ancel", default='c').lower()
82-
if choice != "c":
83-
view = False
84-
try:
85-
if choice == "v":
86-
idx = int(click.prompt("View which .bakfile?", err=err)) - 1
87-
# idx = int(click.prompt("View which .bakfile? (#)")) - 1
88-
view = True
89-
else:
90-
idx = int(choice) - 1
91-
if idx not in _range:
92-
click.echo("Invalid selection. Aborting.", err=err)
93-
return None
94-
else:
95-
if view:
96-
bak_print_cmd(bakfiles[idx])
97-
return
98-
return bakfiles[idx]
99-
except (ValueError, TypeError) as e:
100-
warn(e)
101-
click.echo("Invalid input. Aborting.", err=err)
91+
f"{i + 1}: .bakfile last modified at {bakfiles[i].date_modified}",
92+
err=err)
93+
94+
def get_choice():
95+
return click.prompt(*select_prompt, err=err).lower()
96+
choice = get_choice()
97+
98+
while True:
99+
if choice == "c":
100+
click.echo("Cancelled.", err=err)
102101
return None
103-
else:
104-
click.echo("Aborting.", err=err)
105-
return None
102+
else:
103+
view = False
104+
try:
105+
if choice == "v":
106+
idx = int(click.prompt(
107+
"View which .bakfile?", err=err)) - 1
108+
view = True
109+
elif choice == "d":
110+
idx = int(click.prompt(
111+
"Diff which .bakfile?", err=err)) - 1
112+
bak_diff_cmd(bakfiles[idx])
113+
choice = get_choice()
114+
continue
115+
else:
116+
idx = int(choice) - 1
117+
if idx not in _range:
118+
click.echo("Invalid selection. Aborting.", err=err)
119+
return None
120+
elif view:
121+
bak_print_cmd(bakfiles[idx])
122+
choice = get_choice()
123+
continue
124+
else:
125+
return bakfiles[idx]
126+
except (ValueError, TypeError) as e:
127+
warn(e)
128+
click.echo("Invalid input. Aborting.", err=err)
129+
return None
130+
get_choice()
106131

107132

108133
def show_bak_list(filename: (None, str, os.path) = None):
@@ -140,7 +165,9 @@ def bak_up_cmd(filename: str):
140165
filename (str|os.path)
141166
"""
142167
# Return Truthy things for failures that echo their own output,
143-
# false for nonspecific or generic failures
168+
# false for nonspecific or generic failures.
169+
# Put differently, False is for complete failures. If this function
170+
# handles a failure gracefully, it should return True.
144171

145172
filename = expandpath(filename)
146173
old_bakfile = db_handler.get_bakfile_entries(filename)
@@ -151,19 +178,20 @@ def bak_up_cmd(filename: str):
151178
old_bakfile = old_bakfile[0] if len(old_bakfile) == 1 else \
152179
_do_select_bakfile(old_bakfile)
153180
if old_bakfile is None:
181+
click.echo("Cancelled.")
154182
return True
155183
elif not isinstance(old_bakfile, bakfile.BakFile):
156184
return False
157185

158-
new_bakfile = _assemble_bakfile(filename)
159-
new_bakfile.date_created = old_bakfile.date_created
160-
copy2(new_bakfile.original_file, new_bakfile.bakfile_loc)
161-
db_handler.update_bakfile_entry(old_bakfile, new_bakfile)
186+
old_bakfile.date_modified = datetime.now()
187+
copy2(old_bakfile.original_file, old_bakfile.bakfile_loc)
188+
db_handler.update_bakfile_entry(old_bakfile)
162189
return True
163190

164191

165192
def bak_down_cmd(filename: str,
166-
keep_bakfile: bool = False):
193+
keep_bakfile: bool = False,
194+
quiet: bool = False):
167195
""" Restore `filename` from .bakfile. Prompts if ambiguous (such as
168196
when there are multiple .bakfiles of `filename`)
169197
@@ -173,19 +201,37 @@ def bak_down_cmd(filename: str,
173201
"""
174202
filename = expandpath(filename)
175203
bakfile_entries = db_handler.get_bakfile_entries(filename)
204+
if not bakfile_entries:
205+
click.echo(f"No bakfiles found for {filename}")
206+
return
176207

177-
# TODO still only pulling first result
178208
bakfile_entry = _do_select_bakfile(bakfile_entries) if len(
179209
bakfile_entries) > 1 else bakfile_entries[0]
180210

181211
if not bakfile_entry:
212+
click.echo(f"No bakfiles found for {filename}")
182213
return
183214

184-
os.remove(filename)
185-
copy2(bakfile_entry.bakfile_loc, filename)
215+
if quiet:
216+
confirm = 'y'
217+
else:
218+
confirm_prompt = f"Confirm: Restore {filename} and erase bakfiles?\n" \
219+
if not keep_bakfile else \
220+
f"Confirm: Restore {filename} and keep bakfiles?\n"
221+
confirm_prompt += "(y/n)"
222+
confirm = click.prompt(confirm_prompt, default='n')
223+
if confirm.lower()[0] != 'y':
224+
click.echo("Cancelled.")
225+
return
226+
# os.remove(filename)
227+
# copy2(bakfile_entry.bakfile_loc, filename)
186228
if not keep_bakfile:
229+
os.rename(bakfile_entry.bakfile_loc, filename)
187230
for entry in bakfile_entries:
188-
os.remove(entry.bakfile_loc)
231+
# bakfile_entry's bakfile has already been moved
232+
# trying to rm it would print a failure
233+
if entry != bakfile_entry:
234+
os.remove(entry.bakfile_loc)
189235
db_handler.del_bakfile_entry(entry)
190236

191237

@@ -212,7 +258,8 @@ def bak_off_cmd(filename: (None, str, os.path),
212258
filename = expandpath(filename)
213259
bakfiles = db_handler.get_bakfile_entries(filename)
214260
if not bakfiles:
215-
click.echo(f"No bakfiles found for {filename}")
261+
click.echo(f"No bakfiles found for {os.path.abspath(filename)}")
262+
return False
216263
confirm = input(
217264
f"Confirming: Remove {len(bakfiles)} .bakfiles for {filename}? "
218265
f"(y/N) ").lower() == 'y' if not quietly else True
@@ -223,32 +270,47 @@ def bak_off_cmd(filename: (None, str, os.path),
223270
return False
224271

225272

226-
def bak_print_cmd(bak_to_print: (str, bakfile.BakFile), using: (str, None) = None):
273+
def bak_print_cmd(bak_to_print: (str, bakfile.BakFile),
274+
using: (str, None) = None):
275+
# if this thing is given a string, it needs to go find
276+
# a corresponding bakfile
227277
if not isinstance(bak_to_print, bakfile.BakFile):
228-
bak_to_print = _get_bakfile_entry(bak_to_print,
229-
select_prompt=("View which .bakfile? (#)", "c"))
278+
_bak_to_print = _get_bakfile_entry(bak_to_print,
279+
select_prompt=(
280+
"View which .bakfile? (#)",
281+
"c"))
282+
if not _bak_to_print:
283+
click.echo(
284+
f"No bakfiles found for {os.path.abspath(bak_to_print)}")
285+
else:
286+
bak_to_print = _bak_to_print
230287
if not isinstance(bak_to_print, bakfile.BakFile):
231288
return # _get_bakfile_entry() handles failures, so just exit here
232-
if using:
233-
pager = using
234-
else:
235-
try:
236-
pager = os.environ['PAGER']
237-
except KeyError:
238-
pager = 'less'
239-
call([pager, bak_to_print.bakfile_loc])
289+
pager = using if using else \
290+
(cfg['bak_show_exec'] or os.environ['PAGER']) or 'less'
291+
pager = pager.strip('"').strip("'").split(" ")
292+
call(pager + [bak_to_print.bakfile_loc])
240293

241294

242295
def bak_getfile_cmd(bak_to_get: (str, bakfile.BakFile)):
243296
if not isinstance(bak_to_get, bakfile.BakFile):
297+
filename = bak_to_get
244298
bak_to_get = _get_bakfile_entry(bak_to_get, err=True)
245299
if not bak_to_get:
300+
click.echo(f"No bakfiles found for {os.path.abspath(filename)}")
246301
return # _get_bakfile_entry() handles failures, so just exit
247-
click.echo(bak_to_get.bakfile_loc)
302+
click.echo(bak_to_get.bakfile_loc)
248303

249304

250-
def bak_diff_cmd(filename, command='diff'):
305+
def bak_diff_cmd(filename: str, command='diff'):
251306
# TODO write tests for this (mildly tricky)
252-
bak_to_diff = _get_bakfile_entry(expandpath(filename))
253-
call([command if command else 'diff',
254-
bak_to_diff.bakfile_loc, bak_to_diff.orig_abspath])
307+
bak_to_diff = filename if isinstance(filename, bakfile.BakFile) else \
308+
_get_bakfile_entry(expandpath(filename))
309+
if not command:
310+
command = cfg['bak_diff_exec'] or 'diff'
311+
if not bak_to_diff:
312+
click.echo(f"No bakfiles found for {os.path.abspath(filename)}")
313+
return
314+
command = command.strip('"').strip("'").split(" ")
315+
call(command +
316+
[bak_to_diff.bakfile_loc, bak_to_diff.orig_abspath])

bak/data/bak_db.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,18 @@ def del_bakfile_entry(self, bak_entry: BakFile):
4040

4141
def update_bakfile_entry(self,
4242
old_bakfile: BakFile,
43-
new_bakfile: BakFile):
43+
new_bakfile: (BakFile, None) = None):
4444
with sqlite3.connect(self.db_loc) as db_conn:
4545
db_conn.execute(
4646
"""
4747
DELETE FROM bakfiles WHERE bakfile=:bakfile_loc
4848
""", (old_bakfile.bakfile_loc,))
4949
db_conn.commit()
50-
os.remove(old_bakfile.bakfile_loc)
51-
self.create_bakfile_entry(new_bakfile)
50+
if new_bakfile:
51+
os.remove(old_bakfile.bakfile_loc)
52+
self.create_bakfile_entry(new_bakfile)
53+
else:
54+
self.create_bakfile_entry(old_bakfile)
5255

5356
# TODO handle disambiguation
5457
def get_bakfile_entries(self, filename):

bak/default.cfg

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# NOTE: enclose values in single quotes (see example)
2+
#
3+
# This is currently using CFG (pip install config) and adding
4+
# your own environment variables to these settings
5+
# requires some odd syntax.
6+
#
7+
# This will be addressed before release. For now, if you aren't
8+
# familiar with the module, stick to string values.
9+
10+
bakfile_location: null # Default: $XDG_DATA_HOME/bak/bakfiles
11+
bak_database_location: null # Default: $XDG_DATA_HOME/bak/bak.db
12+
13+
# Example:
14+
# bak_show_exec: 'less'
15+
bak_show_exec: null # Defaults to $PAGER
16+
bak_diff_exec: null # Defaults to system diff

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
click==7.1.2
22
click-default-group==1.2.2
3+
config==0.5.0
34
rich==9.1.0

0 commit comments

Comments
 (0)