|
| 1 | +# Dropbox sync status detection on macOS |
| 2 | + |
| 3 | +Dropbox does not document how to programmatically detect file sync status. This document captures our |
| 4 | +reverse-engineering findings from December 2025. |
| 5 | + |
| 6 | +## Overview |
| 7 | + |
| 8 | +On macOS 12.5+, Dropbox uses Apple's **File Provider API**. Files live in `~/Library/CloudStorage/Dropbox/` (with |
| 9 | +`~/Dropbox` as a symlink). Sync status can be detected via: |
| 10 | + |
| 11 | +1. **File system metadata** (`stat()`) - fast, no API calls |
| 12 | +2. **Spotlight metadata** (`mdls` / `NSMetadataItem`) - slower but more detailed |
| 13 | +3. **Extended attributes** (`com.dropbox.attrs`) - undocumented binary format |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Detection methods |
| 18 | + |
| 19 | +### Method 1: stat() - fastest |
| 20 | + |
| 21 | +The `stat()` system call provides two key indicators: |
| 22 | + |
| 23 | +| Field | Access in Rust | Meaning | |
| 24 | +| ----------- | ----------------------------------------- | ----------------------------------- | |
| 25 | +| `st_blocks` | `metadata.blocks()` | Number of 512-byte blocks allocated | |
| 26 | +| `st_flags` | `metadata.st_flags()` (via `MetadataExt`) | File flags including `SF_DATALESS` | |
| 27 | + |
| 28 | +**Key insight**: Online-only files have `blocks = 0` and `SF_DATALESS` flag set. |
| 29 | + |
| 30 | +```rust |
| 31 | +use std::os::unix::fs::MetadataExt; |
| 32 | + |
| 33 | +const SF_DATALESS: u32 = 0x40000000; |
| 34 | + |
| 35 | +fn is_online_only(metadata: &std::fs::Metadata) -> bool { |
| 36 | + metadata.blocks() == 0 || (metadata.st_flags() & SF_DATALESS != 0) |
| 37 | +} |
| 38 | +``` |
| 39 | + |
| 40 | +### Method 2: Spotlight metadata - for sync progress |
| 41 | + |
| 42 | +Spotlight indexes Dropbox files with these relevant keys: |
| 43 | + |
| 44 | +| Key | Type | Meaning | |
| 45 | +| ---------------------- | ---------- | ---------------------------------------------- | |
| 46 | +| `kMDItemIsUploaded` | Bool (0/1) | File content fully uploaded to Dropbox servers | |
| 47 | +| `kMDItemIsUploading` | Bool (0/1) | File is currently uploading | |
| 48 | +| `kMDItemIsDownloading` | Bool (0/1) | File is currently downloading | |
| 49 | +| `kMDItemIsDownloaded` | Bool | File content fully downloaded (often null) | |
| 50 | + |
| 51 | +**Note**: Spotlight indexing may lag behind actual state. New/temporary files may show `(null)` for all values. |
| 52 | + |
| 53 | +Query via command line: |
| 54 | + |
| 55 | +```bash |
| 56 | +mdls -name kMDItemIsUploaded -name kMDItemIsUploading /path/to/file |
| 57 | +``` |
| 58 | + |
| 59 | +### Method 3: Extended attributes - undocumented |
| 60 | + |
| 61 | +Dropbox stores sync metadata in `com.dropbox.attrs` (26 bytes, binary format): |
| 62 | + |
| 63 | +```bash |
| 64 | +xattr -px com.dropbox.attrs /path/to/file |
| 65 | +# Output: 0A 12 0A 10 FB 39 37 41 16 17 ED F2 00 00 00 00 00 8E E5 7E 10 ... |
| 66 | +``` |
| 67 | + |
| 68 | +The format appears to be a protobuf or custom binary encoding. First 16 bytes are consistent across files (possibly |
| 69 | +account/folder ID). Last 10 bytes vary by file/state. **Not recommended** for detection due to lack of documentation. |
| 70 | + |
| 71 | +--- |
| 72 | + |
| 73 | +## Observed file states |
| 74 | + |
| 75 | +### Synced (downloaded and up-to-date) |
| 76 | + |
| 77 | +``` |
| 78 | +stat: blocks=224, flags=0x40 (64) |
| 79 | +mdls: kMDItemIsUploaded=1, kMDItemIsUploading=0 |
| 80 | +Finder: Green checkmark ✅ |
| 81 | +``` |
| 82 | + |
| 83 | +### Online-only (cloud stub) |
| 84 | + |
| 85 | +``` |
| 86 | +stat: blocks=0, flags=0x40000060 (1073741920) |
| 87 | + Flags include: SF_DATALESS, UF_COMPRESSED, UF_TRACKED |
| 88 | +mdls: kMDItemIsUploaded=1, kMDItemIsUploading=0 |
| 89 | +Finder: Cloud icon ☁️ |
| 90 | +``` |
| 91 | + |
| 92 | +### Uploading (syncing to cloud) |
| 93 | + |
| 94 | +``` |
| 95 | +stat: blocks>0, flags=0x40 (64) |
| 96 | +mdls: kMDItemIsUploaded=0, kMDItemIsUploading=1 |
| 97 | +Finder: Circular arrows 🔄 |
| 98 | +``` |
| 99 | + |
| 100 | +### Downloading (syncing from cloud) |
| 101 | + |
| 102 | +``` |
| 103 | +stat: blocks=0, flags=0x40000060 (SF_DATALESS) |
| 104 | + Size shows expected final size, blocks still 0 |
| 105 | +mdls: All values may be (null) during download |
| 106 | +Finder: Pie chart progress indicator 🥧 |
| 107 | +``` |
| 108 | + |
| 109 | +### Download progress calculation |
| 110 | + |
| 111 | +Finder shows a pie-chart progress indicator during downloads. We can potentially calculate progress using: |
| 112 | + |
| 113 | +``` |
| 114 | +progress = (blocks * 512) / size |
| 115 | +``` |
| 116 | + |
| 117 | +Where: |
| 118 | + |
| 119 | +- `blocks` = `stat().st_blocks` (number of 512-byte blocks allocated) |
| 120 | +- `size` = `stat().st_size` (total file size) |
| 121 | + |
| 122 | +**Caveat**: In our testing, `blocks` remained 0 during download (File Provider may buffer to temp location). This |
| 123 | +approach may not work with Dropbox's File Provider implementation. There is no public API to query per-file download |
| 124 | +progress from a third-party File Provider extension – the `NSProgress` is internal to Dropbox's extension. |
| 125 | + |
| 126 | +**Recommendation**: Show syncing icon 🔄 without progress percentage. Displaying accurate progress would require |
| 127 | +reverse- engineering Dropbox's internal progress mechanism, which is not feasible. |
| 128 | + |
| 129 | +--- |
| 130 | + |
| 131 | +## Recommended detection algorithm |
| 132 | + |
| 133 | +```rust |
| 134 | +pub enum DropboxSyncStatus { |
| 135 | + Synced, // Fully downloaded and uploaded |
| 136 | + OnlineOnly, // Stub file, content in cloud |
| 137 | + Uploading, // Local changes being uploaded |
| 138 | + Downloading, // Cloud content being downloaded |
| 139 | + Unknown, // Not a Dropbox file or status unclear |
| 140 | +} |
| 141 | + |
| 142 | +pub fn get_sync_status(path: &Path) -> DropboxSyncStatus { |
| 143 | + let metadata = match std::fs::metadata(path) { |
| 144 | + Ok(m) => m, |
| 145 | + Err(_) => return DropboxSyncStatus::Unknown, |
| 146 | + }; |
| 147 | + |
| 148 | + let blocks = metadata.blocks(); |
| 149 | + let flags = metadata.st_flags(); |
| 150 | + let is_dataless = (flags & 0x40000000) != 0 || blocks == 0; |
| 151 | + |
| 152 | + // Fast path: check stat() first |
| 153 | + if is_dataless { |
| 154 | + // Could be online-only OR downloading |
| 155 | + // To distinguish, would need Spotlight query (slower) |
| 156 | + return DropboxSyncStatus::OnlineOnly; |
| 157 | + } |
| 158 | + |
| 159 | + // File has local content - check if uploading via Spotlight |
| 160 | + // (This part requires NSMetadataItem query - omitted for brevity) |
| 161 | + // if is_uploading { return DropboxSyncStatus::Uploading; } |
| 162 | + |
| 163 | + DropboxSyncStatus::Synced |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +--- |
| 168 | + |
| 169 | +## File flags reference |
| 170 | + |
| 171 | +Relevant macOS file flags from `<sys/stat.h>`: |
| 172 | + |
| 173 | +| Flag | Value | Meaning | |
| 174 | +| --------------- | ---------- | -------------------------------------- | |
| 175 | +| `UF_NODUMP` | 0x00000001 | Don't dump file | |
| 176 | +| `UF_COMPRESSED` | 0x00000020 | File is compressed | |
| 177 | +| `UF_TRACKED` | 0x00000040 | File changes tracked (File Provider) | |
| 178 | +| `UF_DATAVAULT` | 0x00000080 | Entitlement required for access | |
| 179 | +| `SF_DATALESS` | 0x40000000 | **File is a stub** - content not local | |
| 180 | + |
| 181 | +Online-only Dropbox files typically have flags: `SF_DATALESS | UF_COMPRESSED | UF_TRACKED` = `0x40000060` |
| 182 | + |
| 183 | +--- |
| 184 | + |
| 185 | +## Open questions |
| 186 | + |
| 187 | +1. **Downloading vs online-only**: Both show `blocks=0` and `SF_DATALESS`. During active download, Spotlight metadata |
| 188 | + may be `(null)`. Need to find a reliable way to distinguish "static online-only" from "currently downloading". |
| 189 | + |
| 190 | +2. **Sync errors**: How are sync errors (conflicts, permission issues) indicated? Need test files with error states. |
| 191 | + |
| 192 | +3. **Ignored files**: Files with `com.dropbox.ignored` xattr are not synced. Detection is straightforward: |
| 193 | + |
| 194 | + ```bash |
| 195 | + xattr -p com.dropbox.ignored /path/to/file # Returns "1" if ignored |
| 196 | + ``` |
| 197 | + |
| 198 | +4. **Shared folders**: Do shared folder files have different status indicators? |
| 199 | + |
| 200 | +5. **Spotlight latency**: How quickly does Spotlight update after state changes? Is there a faster notification |
| 201 | + mechanism? |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## Test files used |
| 206 | + |
| 207 | +All in `~/Dropbox/sharing/`: |
| 208 | + |
| 209 | +| File | State | Notes | |
| 210 | +| ------------------------ | ----------- | ----------------------------------- | |
| 211 | +| `edlevo-downloaded.png` | Synced | blocks=224, flags=0x40 | |
| 212 | +| `edlevo-online-only.png` | Online-only | blocks=0, flags=0x40000060 | |
| 213 | +| `edlevo-syncing.png` | Uploading | blocks>0, kMDItemIsUploading=1 | |
| 214 | +| `.../tmp.a` | Downloading | blocks=0, size=571MB, mdls all null | |
| 215 | + |
| 216 | +--- |
| 217 | + |
| 218 | +## References |
| 219 | + |
| 220 | +- Apple File Provider documentation: https://developer.apple.com/documentation/fileprovider |
| 221 | +- Dropbox help article on sync icons: https://help.dropbox.com/sync/icons |
| 222 | +- macOS `stat.h` flags: `/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/stat.h` |
0 commit comments