Skip to content

Commit 9d4cb59

Browse files
committed
Change default datadir location
Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
1 parent 681720d commit 9d4cb59

7 files changed

Lines changed: 167 additions & 5 deletions

File tree

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ jinja2
77
markdown
88
bcrypt
99
iptools
10+
platformdirs

tests/test_config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
from timetagger import config
55
from timetagger._config import set_config
66

7+
import platformdirs
8+
79

810
def test_config():
911
# Defaults
1012
default_bind = "127.0.0.1:8080"
1113
set_config([], {})
14+
15+
expected_default = str(
16+
platformdirs.user_data_path(appname="timetagger", appauthor="Klein").resolve()
17+
)
1218
assert config.bind == default_bind
13-
assert config.datadir == "~/_timetagger"
19+
assert config.datadir == expected_default
1420

1521
# argv
1622
set_config(["--bind=localhost:8080"], {})

tests/test_migrations.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import importlib
2+
import logging
3+
from _common import run_tests
4+
5+
6+
def test_migration_001_happy_path(tmp_path, monkeypatch):
7+
"""Test standard migration where old exists and new does not."""
8+
mig = importlib.import_module("timetagger.migrations.001_datadir_default_xdg")
9+
10+
old_dir = tmp_path / "old" / "_timetagger"
11+
new_dir = tmp_path / "new" / "timetagger"
12+
13+
monkeypatch.setattr(mig, "get_old_default_dir", lambda: old_dir)
14+
monkeypatch.setattr(mig, "get_new_default_dir", lambda: new_dir)
15+
16+
old_dir.mkdir(parents=True)
17+
(old_dir / "jwt.key").write_text("fake key")
18+
19+
mig.run(str(new_dir))
20+
21+
assert not old_dir.exists()
22+
assert new_dir.exists()
23+
assert (new_dir / "jwt.key").read_text() == "fake key"
24+
25+
26+
def test_migration_001_custom_path_skipped(tmp_path, monkeypatch):
27+
"""Test that custom config dirs cause the migration to safely abort."""
28+
mig = importlib.import_module("timetagger.migrations.001_datadir_default_xdg")
29+
30+
old_dir = tmp_path / "old" / "_timetagger"
31+
new_dir = tmp_path / "new" / "timetagger"
32+
custom_dir = tmp_path / "custom" / "timetagger"
33+
34+
monkeypatch.setattr(mig, "get_old_default_dir", lambda: old_dir)
35+
monkeypatch.setattr(mig, "get_new_default_dir", lambda: new_dir)
36+
37+
old_dir.mkdir(parents=True)
38+
mig.run(str(custom_dir))
39+
40+
assert old_dir.exists()
41+
assert not new_dir.exists()
42+
43+
44+
def test_migration_001_conflict_aborts(tmp_path, monkeypatch, caplog):
45+
"""Test that migration aborts if BOTH dirs exist and new has files."""
46+
mig = importlib.import_module("timetagger.migrations.001_datadir_default_xdg")
47+
48+
old_dir = tmp_path / "old" / "_timetagger"
49+
new_dir = tmp_path / "new" / "timetagger"
50+
51+
monkeypatch.setattr(mig, "get_old_default_dir", lambda: old_dir)
52+
monkeypatch.setattr(mig, "get_new_default_dir", lambda: new_dir)
53+
54+
old_dir.mkdir(parents=True)
55+
new_dir.mkdir(parents=True)
56+
(new_dir / "jwt.key").touch()
57+
58+
with caplog.at_level(logging.WARNING):
59+
mig.run(str(new_dir))
60+
61+
assert old_dir.exists()
62+
assert new_dir.exists()
63+
assert "Skipping automatic migration" in caplog.text
64+
65+
66+
def test_migration_001_new_empty_proceeds(tmp_path, monkeypatch):
67+
"""Test that an accidentally created EMPTY new dir is removed safely to allow move."""
68+
mig = importlib.import_module("timetagger.migrations.001_datadir_default_xdg")
69+
70+
old_dir = tmp_path / "old" / "_timetagger"
71+
new_dir = tmp_path / "new" / "timetagger"
72+
73+
monkeypatch.setattr(mig, "get_old_default_dir", lambda: old_dir)
74+
monkeypatch.setattr(mig, "get_new_default_dir", lambda: new_dir)
75+
76+
old_dir.mkdir(parents=True)
77+
(old_dir / "jwt.key").touch()
78+
new_dir.mkdir(parents=True)
79+
80+
mig.run(str(new_dir))
81+
82+
assert not old_dir.exists()
83+
assert (new_dir / "jwt.key").exists()
84+
85+
86+
if __name__ == "__main__":
87+
run_tests(globals())

timetagger/_config.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22
import sys
3+
import importlib
4+
import platformdirs
35

46

57
def to_bool(value):
@@ -24,7 +26,11 @@ class Config:
2426
"""Object that holds config values.
2527
2628
* `bind (str)`: the address and port to bind on. Default "127.0.0.1:8080".
27-
* `datadir (str)`: the directory to store data. Default "~/_timetagger".
29+
* `datadir (str)`: the directory to store data.
30+
Default depends on the OS
31+
- ~/.local/share/timetagger
32+
- ~/Library/Application Support/timetagger
33+
- C:\\Users\\<User>\\AppData\\Local\\Klein\\timetagger
2834
The user db's are stored in `datadir/users`.
2935
* `log_level (str)`: the log level for timetagger and asgineer
3036
(not the asgi server). Default "info".
@@ -57,7 +63,13 @@ class Config:
5763

5864
_ITEMS = [
5965
("bind", str, "127.0.0.1:8080"),
60-
("datadir", str, "~/_timetagger"),
66+
(
67+
"datadir",
68+
str,
69+
os.path.realpath(
70+
platformdirs.user_data_dir(appname="timetagger", appauthor="Klein")
71+
),
72+
),
6173
("log_level", str, "info"),
6274
("credentials", str, ""),
6375
("proxy_auth_enabled", to_bool, False),
@@ -123,3 +135,8 @@ def _update_config_from_env(env):
123135

124136
# Init config
125137
set_config()
138+
139+
# Run datadir migration
140+
importlib.import_module("timetagger.migrations.001_datadir_default_xdg").run(
141+
config.datadir
142+
)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import shutil
2+
import logging
3+
from pathlib import Path
4+
import platformdirs
5+
6+
7+
def get_new_default_dir() -> Path:
8+
return platformdirs.user_data_path(
9+
appname="timetagger", appauthor="Klein"
10+
).resolve()
11+
12+
13+
def get_old_default_dir() -> Path:
14+
return (Path.home() / "_timetagger").resolve()
15+
16+
17+
def run(datadir: str):
18+
new_default = get_new_default_dir()
19+
old_default = get_old_default_dir()
20+
21+
current_cfg = Path(datadir).expanduser().resolve()
22+
23+
# skip if user is using some custom non-default path
24+
if current_cfg != new_default:
25+
return
26+
27+
# skip if there is no old directory to migrate
28+
if not old_default.exists() or not old_default.is_dir():
29+
return
30+
31+
# handle conflicts if the new directory already exists
32+
if new_default.exists():
33+
if any(new_default.iterdir()):
34+
logging.warning(
35+
f"Notice: Both old ({old_default}) and new ({new_default}) "
36+
"data directories exist. Skipping automatic migration to prevent overwriting."
37+
)
38+
return
39+
else:
40+
# if it's completely empty, safely remove it to allow the move
41+
new_default.rmdir()
42+
43+
try:
44+
logging.info(
45+
f"Migrating data from {old_default} to standard location: {new_default}..."
46+
)
47+
new_default.parent.mkdir(parents=True, exist_ok=True)
48+
shutil.move(str(old_default), str(new_default))
49+
logging.info("Data directory migration successful.")
50+
except Exception as e:
51+
logging.error(f"Failed to migrate data directory: {e}")

timetagger/migrations/__init__.py

Whitespace-only changes.

timetagger/server/_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
from .. import config
1414

1515
# Init directory paths
16-
ROOT_TT_DIR = os.path.expanduser(config.datadir)
16+
ROOT_TT_DIR = config.datadir
1717
ROOT_USER_DIR = os.path.join(ROOT_TT_DIR, "users")
1818
if not os.path.isdir(ROOT_USER_DIR):
19-
os.makedirs(ROOT_USER_DIR)
19+
os.makedirs(ROOT_USER_DIR, exist_ok=True)
2020

2121
# Init logger
2222
logger = logging.getLogger("asgineer")

0 commit comments

Comments
 (0)