feat: initial Dropbox tag sync tool
CI / Tests (Linux, Python 3.12) (push) Failing after 4m51s
CI / Tests (Linux, Python 3.11) (push) Failing after 5m19s
CI / Tests (Linux, Python 3.9) (push) Failing after 5m3s
CI / Tests (macOS) (push) Has been cancelled
CI / Tests (Windows) (push) Has been cancelled

Cross-platform CLI that mirrors Dropbox web tags to OS-native tags:
- macOS: Finder xattrs (content-preserving, no re-upload)
- Windows: XMP:Subject via ExifTool (re-upload expected once per file)

Includes an interactive wizard that auto-detects the OS and Dropbox
folder, walks the user through read-only token creation, previews
tagged files, applies tags, and installs a background scheduler
(launchd on macOS, Task Scheduler on Windows).

Ships with:
- 47-test suite (pytest) covering config, state DB, run_sync, writers
- Gitea Actions CI for Linux / macOS / Windows with coverage reporting
This commit is contained in:
2026-04-20 14:16:50 -04:00
commit e363020080
21 changed files with 2626 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
"""Tests for tagsync.platform_windows: sidecar writer + support matrix.
Runs on any OS — the Windows-specific online-only stat test is skipped
except on Windows.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from tagsync.platform_windows import (
XMP_WRITABLE_EXTS,
WindowsWriter,
)
# --- supports() --------------------------------------------------------------
def test_supports_xmp_formats():
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
for ext in (".jpg", ".pdf", ".docx", ".mp4"):
assert w.supports(Path(f"foo{ext}")), f"expected support for {ext}"
def test_supports_rejects_unknown_without_sidecars():
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
assert not w.supports(Path("archive.zip"))
assert not w.supports(Path("notes.txt"))
def test_supports_accepts_unknown_with_sidecars():
w = WindowsWriter(exiftool="exiftool", write_sidecars=True)
assert w.supports(Path("archive.zip"))
assert w.supports(Path("notes.txt"))
def test_supports_is_case_insensitive():
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
assert w.supports(Path("IMAGE.JPG"))
assert w.supports(Path("report.PDF"))
# --- sidecar writer ---------------------------------------------------------
def test_sidecar_written_for_unsupported_type(tmp_path):
w = WindowsWriter(exiftool="exiftool", write_sidecars=True)
target = tmp_path / "archive.zip"
target.touch()
w.write_tags(target, ["project-alpha", "urgent"])
sidecar = tmp_path / "archive.zip.tags.json"
assert sidecar.exists()
data = json.loads(sidecar.read_text())
assert data == {"tags": ["project-alpha", "urgent"]}
def test_sidecar_cleared_when_tags_empty(tmp_path):
w = WindowsWriter(exiftool="exiftool", write_sidecars=True)
target = tmp_path / "archive.zip"
target.touch()
(tmp_path / "archive.zip.tags.json").write_text('{"tags": ["x"]}')
w.write_tags(target, [])
assert not (tmp_path / "archive.zip.tags.json").exists()
def test_sidecar_clear_nonexistent_is_ok(tmp_path):
w = WindowsWriter(exiftool="exiftool", write_sidecars=True)
target = tmp_path / "archive.zip"
target.touch()
# Should not raise even though no sidecar exists.
w.write_tags(target, [])
def test_unsupported_without_sidecars_raises(tmp_path):
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
target = tmp_path / "archive.zip"
target.touch()
with pytest.raises(RuntimeError):
w.write_tags(target, ["tag"])
# --- exiftool invocation ----------------------------------------------------
#
# We don't actually run exiftool; monkeypatch subprocess.run to capture args.
def test_exiftool_args_are_well_formed(tmp_path, monkeypatch):
captured = {}
def fake_run(args, **kwargs):
captured["args"] = args
class R:
returncode = 0
stderr = ""
return R()
import tagsync.platform_windows as pw
monkeypatch.setattr(pw.subprocess, "run", fake_run)
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
target = tmp_path / "pic.jpg"
target.touch()
w.write_tags(target, ["alpha", "beta"])
args = captured["args"]
assert args[0] == "exiftool"
assert "-overwrite_original" in args
# Subject should be cleared then appended for each tag.
assert "-XMP-dc:Subject=" in args
assert "-XMP-dc:Subject+=alpha" in args
assert "-XMP-dc:Subject+=beta" in args
assert str(target) in args
def test_exiftool_failure_raises(tmp_path, monkeypatch):
def fake_run(args, **kwargs):
class R:
returncode = 1
stderr = "bad things happened"
return R()
import tagsync.platform_windows as pw
monkeypatch.setattr(pw.subprocess, "run", fake_run)
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
target = tmp_path / "pic.jpg"
target.touch()
with pytest.raises(RuntimeError, match="bad things happened"):
w.write_tags(target, ["x"])
# --- online-only detection --------------------------------------------------
@pytest.mark.windows
def test_is_online_only_returns_false_for_normal_files(tmp_path):
w = WindowsWriter(exiftool="exiftool")
f = tmp_path / "regular.txt"
f.touch()
assert w.is_online_only(f) is False
def test_is_online_only_returns_false_when_attribute_unavailable(tmp_path):
"""On non-Windows, st_file_attributes doesn't exist -> should return False."""
w = WindowsWriter(exiftool="exiftool")
f = tmp_path / "regular.txt"
f.touch()
assert w.is_online_only(f) is False