feat: add PowerShell port for Windows users without Python
CI / Tests (Linux, Python 3.12) (push) Successful in 9m24s
CI / Tests (Linux, Python 3.12) (push) Successful in 9m24s
Adds tagsync.ps1, a self-contained PowerShell rewrite of the full tagsync tool. Uses Invoke-RestMethod for Dropbox REST calls, DPAPI- encrypted file for token storage, JSON for state, and native Register-ScheduledTask for scheduling. Only external dependency is exiftool.exe (same as the Python version). Updates README to lead with the PowerShell quick start, with Python instructions in a separate section below.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Dropbox Tag Sync
|
||||
|
||||
Bring Dropbox web tags down to your computer so you can search for tagged
|
||||
files in Finder (macOS) or File Explorer (Windows) as if they were
|
||||
files in File Explorer (Windows) or Finder (macOS) as if they were
|
||||
first-class local tags.
|
||||
|
||||
## The problem this solves
|
||||
@@ -17,74 +17,149 @@ This tool closes that gap. It connects to Dropbox, reads each file's
|
||||
tags, and writes them to your local files in a form your operating
|
||||
system treats as a first-class tag.
|
||||
|
||||
## Quick start
|
||||
---
|
||||
|
||||
## Windows — PowerShell (no Python required)
|
||||
|
||||
**Requirements:** Windows 10/11, PowerShell 5.1+, [ExifTool](https://exiftool.org/) on your PATH.
|
||||
|
||||
### Quick start
|
||||
|
||||
```powershell
|
||||
.\tagsync.ps1 wizard
|
||||
```
|
||||
|
||||
If you get an execution policy error, run this first (once, per machine):
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
All commands are run as `.\tagsync.ps1 <command>`.
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `wizard` | Interactive setup (run this first) |
|
||||
| `run` | One-shot sync using saved settings |
|
||||
| `run -DryRun` | Show what would change without touching files |
|
||||
| `run -Verbose` | Show every file action during sync |
|
||||
| `status` | Show config, token presence, and schedule state |
|
||||
| `schedule install` | Install the background scheduled task |
|
||||
| `schedule install -Interval 30` | Install, running every 30 minutes |
|
||||
| `schedule uninstall` | Remove the background scheduled task |
|
||||
| `schedule status` | Show whether the scheduled task is installed |
|
||||
| `reset -All` | Clear stored token, settings, and local state |
|
||||
| `reset -Token` | Clear only the token |
|
||||
|
||||
### Where things are stored (Windows / PowerShell)
|
||||
|
||||
| Item | Location |
|
||||
|---|---|
|
||||
| Token | `~\.dropbox_tag_sync\token.dpapi` (DPAPI-encrypted, current user only) |
|
||||
| Settings | `~\.dropbox_tag_sync\config.json` |
|
||||
| Local state cache | `~\.dropbox_tag_sync\state.json` |
|
||||
| Scheduled task | Task Scheduler, name: `DropboxTagSync` |
|
||||
|
||||
### Troubleshooting (Windows / PowerShell)
|
||||
|
||||
**"exiftool not found"** — install from <https://exiftool.org/>, place
|
||||
`exiftool.exe` on your PATH (e.g. `C:\Windows\`), then confirm
|
||||
`exiftool -ver` works in a terminal.
|
||||
|
||||
**Windows tags don't show in Explorer** — Explorer's Tags column may
|
||||
be hidden; right-click a column header and add **Tags**. Windows Search
|
||||
may need a few minutes to re-index.
|
||||
|
||||
**"not found locally"** — the file exists in Dropbox but hasn't synced
|
||||
down yet. Let Dropbox finish syncing, or mark the file as "always keep
|
||||
on this device" if it is online-only.
|
||||
|
||||
**Token wrong or revoked** — run `.\tagsync.ps1 reset -Token`, then
|
||||
`.\tagsync.ps1 wizard` to enter a new one.
|
||||
|
||||
**State got into a weird state** — run `.\tagsync.ps1 reset -State`.
|
||||
The next run will re-examine every file; no data is lost.
|
||||
|
||||
---
|
||||
|
||||
## macOS / Windows — Python
|
||||
|
||||
**Requirements:** Python 3.9+, packages in `requirements.txt`, and
|
||||
(Windows only) [ExifTool](https://exiftool.org/) on your PATH.
|
||||
|
||||
### Quick start
|
||||
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
python tagsync.py wizard
|
||||
```
|
||||
|
||||
That's it. The wizard detects your OS, finds your Dropbox folder, walks
|
||||
you through creating a read-only Dropbox access token, previews what
|
||||
will be tagged, applies the tags, and offers to install a background
|
||||
scheduler so new tags keep flowing down automatically.
|
||||
The wizard detects your OS, finds your Dropbox folder, walks you through
|
||||
creating a read-only Dropbox access token, previews what will be tagged,
|
||||
applies the tags, and offers to install a background scheduler.
|
||||
|
||||
Everything after that is optional — if you want to re-run a sync,
|
||||
check status, or manage the schedule, use the other subcommands below.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Python 3.9+**
|
||||
- The packages in `requirements.txt` (`pip install -r requirements.txt`)
|
||||
- **Windows only:** [ExifTool](https://exiftool.org/) on your `PATH`
|
||||
|
||||
## What's in this repo
|
||||
|
||||
| File / folder | Purpose |
|
||||
| ------------------------ | ------------------------------------------ |
|
||||
| `tagsync.py` | Single CLI entry point |
|
||||
| `tagsync/` | Package with all the logic |
|
||||
| `requirements.txt` | Python dependencies |
|
||||
| `README.md` | This document |
|
||||
|
||||
Inside `tagsync/`:
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------- | --------------------------------------------- |
|
||||
| `wizard.py` | Interactive setup flow |
|
||||
| `core.py` | Dropbox listing, tag fetching, sync loop |
|
||||
| `config.py` | Settings file + secure token storage |
|
||||
| `detect.py` | OS / Dropbox folder / dependency detection |
|
||||
| `platform_macos.py` | Finder-xattr writer + launchd scheduler |
|
||||
| `platform_windows.py` | ExifTool-based XMP writer + Task Scheduler |
|
||||
|
||||
## Commands
|
||||
### Commands
|
||||
|
||||
All commands are run as `python tagsync.py <command>`.
|
||||
|
||||
| Command | What it does |
|
||||
| ------------------------------ | ---------------------------------------------------- |
|
||||
| `wizard` | Interactive setup (run this first) |
|
||||
| `run` | Perform a one-shot sync using saved settings |
|
||||
| `run --dry-run` | Show what would change without touching any files |
|
||||
| `status` | Show config, token presence, and schedule state |
|
||||
| `schedule install` | Install the background scheduled task |
|
||||
| `schedule install --interval 30` | Install, running every 30 minutes |
|
||||
| `schedule uninstall` | Remove the background scheduled task |
|
||||
| `schedule status` | Show whether the scheduler is installed and loaded |
|
||||
| `reset --all` | Clear stored token, settings, and local state DB |
|
||||
| `reset --token` | Clear only the token |
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `wizard` | Interactive setup (run this first) |
|
||||
| `run` | Perform a one-shot sync using saved settings |
|
||||
| `run --dry-run` | Show what would change without touching any files |
|
||||
| `status` | Show config, token presence, and schedule state |
|
||||
| `schedule install` | Install the background scheduled task |
|
||||
| `schedule install --interval 30` | Install, running every 30 minutes |
|
||||
| `schedule uninstall` | Remove the background scheduled task |
|
||||
| `schedule status` | Show whether the scheduler is installed and loaded |
|
||||
| `reset --all` | Clear stored token, settings, and local state DB |
|
||||
| `reset --token` | Clear only the token |
|
||||
|
||||
### What's in this repo
|
||||
|
||||
| File / folder | Purpose |
|
||||
|---|---|
|
||||
| `tagsync.ps1` | PowerShell entry point (Windows, no Python) |
|
||||
| `tagsync.py` | Python CLI entry point |
|
||||
| `tagsync/` | Python package with all the logic |
|
||||
| `requirements.txt` | Python dependencies |
|
||||
|
||||
Inside `tagsync/`:
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `wizard.py` | Interactive setup flow |
|
||||
| `core.py` | Dropbox listing, tag fetching, sync loop |
|
||||
| `config.py` | Settings file + secure token storage |
|
||||
| `detect.py` | OS / Dropbox folder / dependency detection |
|
||||
| `platform_macos.py` | Finder-xattr writer + launchd scheduler |
|
||||
| `platform_windows.py` | ExifTool-based XMP writer + Task Scheduler |
|
||||
|
||||
### Where things are stored (Python)
|
||||
|
||||
| Item | Location |
|
||||
|---|---|
|
||||
| Token (preferred) | macOS Keychain / Windows Credential Manager |
|
||||
| Token (fallback) | `~/.dropbox_tag_sync/token` (mode 0600) |
|
||||
| Settings | `~/.dropbox_tag_sync/config.json` |
|
||||
| Local state cache | `~/.dropbox_tag_sync/state.db` |
|
||||
| Log (macOS) | `~/Library/Logs/dropbox-tag-sync.log` |
|
||||
| launchd plist | `~/Library/LaunchAgents/com.dropboxtagsync.agent.plist` |
|
||||
| Windows task | Task Scheduler, name: `DropboxTagSync` |
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
1. You create a Dropbox access token with **read-only** metadata scope.
|
||||
The wizard walks you through this. The token lives in your OS
|
||||
keyring (macOS Keychain or Windows Credential Manager).
|
||||
The wizard walks you through this.
|
||||
2. The tool lists every file in your Dropbox and asks Dropbox which
|
||||
tags each file has.
|
||||
3. For files whose tags have changed since the last run, it writes
|
||||
those tags to the local copy.
|
||||
4. A small SQLite cache remembers what was written so subsequent runs
|
||||
4. A small state cache remembers what was written so subsequent runs
|
||||
only touch files whose tags actually changed.
|
||||
|
||||
Nothing is ever deleted from Dropbox. The tool only **reads** from
|
||||
@@ -92,14 +167,13 @@ Dropbox and **writes** to local files on your machine.
|
||||
|
||||
## The important platform difference
|
||||
|
||||
macOS and Windows store "file tags" in fundamentally different ways,
|
||||
and that shapes what happens under the hood.
|
||||
macOS and Windows store "file tags" in fundamentally different ways.
|
||||
|
||||
### macOS — clean and invisible
|
||||
|
||||
macOS has a universal, built-in tag system. Tags are stored in an
|
||||
**extended attribute** (`xattr`) alongside the file, not inside it. The
|
||||
tool writes the xattr directly. Consequences:
|
||||
**extended attribute** (`xattr`) alongside the file, not inside it.
|
||||
The tool writes the xattr directly. Consequences:
|
||||
|
||||
- Every file type can be tagged (text, zip, source code, anything).
|
||||
- The file's contents are untouched, so Dropbox does **not** see the
|
||||
@@ -110,7 +184,6 @@ tool writes the xattr directly. Consequences:
|
||||
|
||||
Windows has no universal tag store. The "Tags" field in Explorer is
|
||||
actually the **XMP:Subject** metadata field embedded inside the file.
|
||||
This is a limitation of the operating system, not the tool.
|
||||
|
||||
Consequences:
|
||||
|
||||
@@ -119,19 +192,15 @@ Consequences:
|
||||
by default and can optionally write a `.tags.json` sidecar next to
|
||||
them instead.
|
||||
- Because the tag lives inside the file, writing it **does** modify
|
||||
the file. Dropbox will notice and re-upload the file once. After
|
||||
that first sync, tags are stored in both the cloud and the local
|
||||
copy, and future runs only touch files whose Dropbox tags change
|
||||
again.
|
||||
the file. Dropbox will notice and re-upload the file once.
|
||||
- If Dropbox Smart Sync leaves some files online-only, tagging one
|
||||
would trigger a download. The tool can skip online-only files (on
|
||||
by default in the wizard).
|
||||
|
||||
> **Plan for the first Windows run.** If there are many tagged files,
|
||||
> the first run will queue a large number of re-uploads as each file
|
||||
> gets its tags embedded. Schedule it overnight, on a good connection,
|
||||
> and expect a one-time spike in Dropbox activity. Subsequent runs are
|
||||
> quiet.
|
||||
> the first run will queue a large number of re-uploads. Schedule it
|
||||
> overnight, on a good connection, and expect a one-time spike in
|
||||
> Dropbox activity. Subsequent runs are quiet.
|
||||
|
||||
## One-time Dropbox app setup
|
||||
|
||||
@@ -143,8 +212,7 @@ reference:
|
||||
- **Scoped access**
|
||||
- **Full Dropbox**
|
||||
- A name of your choosing (e.g. `tag-sync`)
|
||||
3. Open the **Permissions** tab and enable:
|
||||
- `files.metadata.read`
|
||||
3. Open the **Permissions** tab and enable `files.metadata.read`.
|
||||
4. Click **Submit**.
|
||||
5. On the **Settings** tab, scroll to **OAuth 2 → Generated access token**
|
||||
and click **Generate**. Paste that token into the wizard.
|
||||
@@ -152,39 +220,6 @@ reference:
|
||||
The generated token only has permission to read file metadata. It
|
||||
cannot modify anything in Dropbox even if it leaked.
|
||||
|
||||
## Where things are stored
|
||||
|
||||
| Item | Location |
|
||||
| ------------------ | ---------------------------------------------------- |
|
||||
| Token (preferred) | macOS Keychain / Windows Credential Manager |
|
||||
| Token (fallback) | `~/.dropbox_tag_sync/token` (mode 0600) |
|
||||
| Settings | `~/.dropbox_tag_sync/config.json` |
|
||||
| Local state cache | `~/.dropbox_tag_sync/state.db` |
|
||||
| Log (macOS) | `~/Library/Logs/dropbox-tag-sync.log` |
|
||||
| launchd plist | `~/Library/LaunchAgents/com.dropboxtagsync.agent.plist` |
|
||||
| Windows task | Task Scheduler, name: `DropboxTagSync` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"not found locally"** — the file exists in Dropbox but hasn't synced
|
||||
down yet. Let Dropbox finish syncing, or (on Windows) mark the file
|
||||
as "always keep on this device" if it's online-only.
|
||||
|
||||
**Windows tags don't show up in Explorer after a run** — Explorer's
|
||||
Tags column may be hidden; right-click a column header and add
|
||||
**Tags**. Windows Search may need a few minutes to re-index.
|
||||
|
||||
**"exiftool not found"** — install it from <https://exiftool.org/>
|
||||
and confirm `exiftool -ver` works in a terminal.
|
||||
|
||||
**The state cache got into a weird state** — run
|
||||
`python tagsync.py reset --state`. The next run will re-examine every
|
||||
file; no data is lost.
|
||||
|
||||
**The token is wrong or revoked** — run
|
||||
`python tagsync.py reset --token`, then `python tagsync.py wizard`
|
||||
to enter a new one.
|
||||
|
||||
## Safety summary
|
||||
|
||||
- **Read-only** with Dropbox (token scope: `files.metadata.read`).
|
||||
|
||||
Reference in New Issue
Block a user