A reusable library for backing up files to Dropbox and syncing from Dropbox with configurable patterns and exclusions
Project description
Dropbox Backup Library
A reusable Python library for backing up files to Dropbox and syncing files from Dropbox with configurable patterns and exclusions.
Features
- True bidirectional sync: Automatically syncs changes in both directions with intelligent conflict resolution
- Backup mode: One-way upload from local to Dropbox
- OAuth2 refresh token authentication (tokens never expire)
- Configurable file patterns for inclusion and exclusion
- Smart conflict resolution (timestamp, local wins, remote wins, or prompt)
- Tracks sync state to detect changes since last sync
- Progress tracking and callbacks
- Easy integration into any Python project
- Command-line tools for authentication and syncing
- Dry-run mode to preview changes before applying
- Type hints and comprehensive documentation
Installation
For Development (Editable Install)
cd ~/projects/dropbox-backup-library
pip install -e .
This installs the package in "editable" mode, so changes to the source code are immediately available.
For Production
pip install git+https://github.com/clayroach/dropbox-backup-library.git
Or if you publish to PyPI:
pip install dropbox-backup-lib
Quick Start
Interactive Tutorial (Recommended)
The fastest way to get started is to run the interactive getting started example:
cd examples
python3 getting_started.py
This will walk you through:
- Setting up your Dropbox app credentials
- Generating an OAuth refresh token
- Testing backup functionality
- Testing sync functionality
- Verifying everything works
Manual Setup
If you prefer to set things up manually:
1. Set Up Dropbox App
- Go to Dropbox App Console
- Click "Create app"
- Choose:
- API: Scoped API
- Type: Full Dropbox
- Name: Your app name (e.g., "my-project-backup")
- Go to "Permissions" tab and enable:
files.metadata.readfiles.content.readfiles.content.write
- Go to "Settings" tab and note your App key and App secret
2. Generate Refresh Token
Create a .env file with your app credentials:
DROPBOX_APP_KEY=your_app_key_here
DROPBOX_APP_SECRET=your_app_secret_here
DROPBOX_REFRESH_TOKEN=
Then run the token generator:
dropbox-get-token
This will guide you through the OAuth flow and save the refresh token to your .env file.
CLI Tool for Syncing
You can use the command-line tool for true bidirectional sync:
# Add sync configuration to .env
BACKUP_DROPBOX_PATH=/backups/my-project
BACKUP_SOURCE_DIR=./local-copy
# Run bidirectional sync
dropbox-sync
# Preview changes without applying them
dropbox-sync --dry-run
The CLI will:
- Detect changes in both local directory and Dropbox
- Upload local changes to Dropbox
- Download Dropbox changes to local
- Handle conflicts based on your chosen strategy
- Track sync state to know what changed since last sync
- Show progress and summary
3. Use in Your Code
Backup to Dropbox:
from pathlib import Path
from dotenv import load_dotenv
from dropbox_backup_lib import BackupConfig, DropboxBackup
# Load environment variables
load_dotenv()
# Configure backup
config = BackupConfig.from_env(
source_dir=Path.cwd(),
dropbox_path='/backups/my-project',
backup_patterns=[
'*.md',
'*.py',
'*.xlsx',
'data/**/*.csv',
],
exclude_patterns=[
'.git/*',
'*.pyc',
'.env*',
]
)
# Run backup
backup = DropboxBackup(config)
result = backup.run()
print(f"Backed up {result.success_count} files")
if result.failed_count > 0:
print(f"Failed: {result.failed_count}")
Bidirectional Sync (recommended):
from pathlib import Path
from dotenv import load_dotenv
from dropbox_backup_lib import BidirectionalSyncConfig, BidirectionalSync, ConflictStrategy
# Load environment variables
load_dotenv()
# Configure bidirectional sync
config = BidirectionalSyncConfig.from_env(
local_dir=Path.cwd() / 'synced-files',
dropbox_path='/backups/my-project',
conflict_strategy=ConflictStrategy.NEWER_WINS
)
# Run sync
sync = BidirectionalSync(config)
result = sync.run()
print(f"Uploaded {len(result.uploaded)}, Downloaded {len(result.downloaded)}")
if result.conflicts:
print(f"Conflicts: {len(result.conflicts)}")
One-way download from Dropbox (legacy):
from pathlib import Path
from dotenv import load_dotenv
from dropbox_backup_lib import SyncConfig, DropboxSync, ConflictResolution
# Load environment variables
load_dotenv()
# Configure sync
config = SyncConfig.from_env(
dropbox_path='/backups/my-project',
local_dir=Path.cwd() / 'synced-files',
conflict_resolution=ConflictResolution.TIMESTAMP
)
# Run sync
sync = DropboxSync(config)
result = sync.run()
print(f"Downloaded {result.downloaded_count} files")
if result.skipped_count > 0:
print(f"Skipped {result.skipped_count} (local files newer)")
API Reference
BackupConfig
Configuration object for backup operations.
from dropbox_backup_lib import BackupConfig
# Create from environment variables
config = BackupConfig.from_env(
source_dir=Path('/path/to/source'),
dropbox_path='/destination/in/dropbox'
)
# Or create manually
config = BackupConfig(
app_key='your_app_key',
app_secret='your_app_secret',
refresh_token='your_refresh_token',
source_dir=Path('/path/to/source'),
dropbox_path='/destination/in/dropbox',
backup_patterns=['*.md', '*.py'],
exclude_patterns=['.git/*', '*.pyc']
)
Parameters:
app_key(str): Dropbox app keyapp_secret(str): Dropbox app secretrefresh_token(str): OAuth2 refresh tokensource_dir(Path): Directory to backup fromdropbox_path(str): Destination path in Dropboxbackup_patterns(List[str]): Glob patterns for files to includeexclude_patterns(List[str]): Glob patterns for files to exclude
DropboxBackup
Main backup class.
from dropbox_backup_lib import DropboxBackup
backup = DropboxBackup(config)
# Run backup with default settings
result = backup.run()
# Run with custom progress callback
def on_progress(file_path, success, message):
print(f"{'✓' if success else '✗'} {file_path}")
result = backup.run(progress_callback=on_progress, verbose=False)
# Access results
print(f"Success: {result.success_count}")
print(f"Failed: {result.failed_count}")
print(f"Total: {result.total_count}")
SyncConfig
Configuration object for sync (download) operations.
from dropbox_backup_lib import SyncConfig, ConflictResolution
# Create from environment variables
config = SyncConfig.from_env(
dropbox_path='/backups/my-project',
local_dir=Path('/path/to/local')
)
# Or create manually with all options
config = SyncConfig(
app_key='your_app_key',
app_secret='your_app_secret',
refresh_token='your_refresh_token',
dropbox_path='/backups/my-project',
local_dir=Path('/path/to/local'),
sync_patterns=['*.md', '*.py'], # None = sync all files
exclude_patterns=['.git/*', '*.pyc'],
conflict_resolution=ConflictResolution.TIMESTAMP,
track_modifications=True
)
Parameters:
app_key(str): Dropbox app keyapp_secret(str): Dropbox app secretrefresh_token(str): OAuth2 refresh tokendropbox_path(str): Source path in Dropbox to sync fromlocal_dir(Path): Local directory to sync tosync_patterns(List[str] | None): Glob patterns for files to include (None = all files)exclude_patterns(List[str]): Glob patterns for files to excludeconflict_resolution(ConflictResolution): Strategy for handling conflictsConflictResolution.OVERWRITE: Always overwrite local filesConflictResolution.SKIP: Keep local files, skip downloadConflictResolution.TIMESTAMP: Download only if Dropbox file is newerConflictResolution.PROMPT: Ask user for each conflict
track_modifications(bool): Track local file modifications since last sync
BidirectionalSyncConfig
Configuration object for true bidirectional sync operations.
from dropbox_backup_lib import BidirectionalSyncConfig, ConflictStrategy
# Create from environment variables
config = BidirectionalSyncConfig.from_env(
local_dir=Path('/path/to/local'),
dropbox_path='/backups/my-project'
)
# Or create manually with all options
config = BidirectionalSyncConfig(
app_key='your_app_key',
app_secret='your_app_secret',
refresh_token='your_refresh_token',
local_dir=Path('/path/to/local'),
dropbox_path='/backups/my-project',
sync_patterns=['*.md', '*.py'], # None = sync all files
exclude_patterns=['.git/*', '*.pyc'],
conflict_strategy=ConflictStrategy.NEWER_WINS
)
Parameters:
app_key(str): Dropbox app keyapp_secret(str): Dropbox app secretrefresh_token(str): OAuth2 refresh tokenlocal_dir(Path): Local directory to syncdropbox_path(str): Dropbox path to syncsync_patterns(List[str] | None): Glob patterns for files to include (None = all files)exclude_patterns(List[str]): Glob patterns for files to excludeconflict_strategy(ConflictStrategy): Strategy for handling three-way conflictsConflictStrategy.LOCAL_WINS: Always prefer local versionConflictStrategy.REMOTE_WINS: Always prefer Dropbox versionConflictStrategy.NEWER_WINS: Use timestamp to decide (default)ConflictStrategy.PROMPT: Ask user for each conflict
state_file(Path): Path to file tracking sync state (default: local_dir/.sync_state.json)
BidirectionalSync
Main class for true bidirectional sync between local directory and Dropbox.
from dropbox_backup_lib import BidirectionalSync, ConflictStrategy, SyncDirection
sync = BidirectionalSync(config)
# Run sync with default settings
result = sync.run()
# Run with dry-run to preview changes
result = sync.run(dry_run=True)
# Run with custom conflict callback
def handle_conflict(path, local_mtime, remote_mtime):
print(f"Conflict: {path}")
return SyncDirection.UPLOAD # or DOWNLOAD or SKIP
result = sync.run(
conflict_callback=handle_conflict,
verbose=True
)
# Access results
print(f"Uploaded: {len(result.uploaded)}")
print(f"Downloaded: {len(result.downloaded)}")
print(f"Conflicts: {len(result.conflicts)}")
print(f"Failed: {len(result.failed)}")
DropboxSync
Main sync class for one-way download from Dropbox (legacy).
from dropbox_backup_lib import DropboxSync, ConflictResolution
sync = DropboxSync(config)
# Run sync with default settings
result = sync.run()
# Run with custom conflict callback
def handle_conflict(local_path, local_mtime, dropbox_mtime):
print(f"Conflict: {local_path}")
return True # True to download, False to skip
result = sync.run(
conflict_callback=handle_conflict,
verbose=True
)
# Access results
print(f"Downloaded: {result.downloaded_count}")
print(f"Skipped: {result.skipped_count}")
print(f"Failed: {result.failed_count}")
DropboxAuthHelper
Helper for OAuth2 authentication flow.
from dropbox_backup_lib import DropboxAuthHelper
# Create from environment
helper = DropboxAuthHelper.from_env()
# Get authorization URL
url = helper.get_authorization_url()
print(f"Visit: {url}")
# Get auth code from user
auth_code = input("Enter code: ")
# Get refresh token
refresh_token = helper.finish_auth(auth_code)
# Save to .env file
from pathlib import Path
helper.update_env_file(Path('.env'), refresh_token)
Examples
Basic Backup Script
Create a file backup.py:
#!/usr/bin/env python3
from pathlib import Path
from dotenv import load_dotenv
from dropbox_backup_lib import BackupConfig, DropboxBackup
load_dotenv()
config = BackupConfig.from_env(
source_dir=Path(__file__).parent,
dropbox_path='/backups/my-project'
)
backup = DropboxBackup(config)
result = backup.run()
exit(0 if result.failed_count == 0 else 1)
Make it executable:
chmod +x backup.py
./backup.py
Custom Progress Tracking
from dropbox_backup_lib import DropboxBackup, BackupConfig
def progress_handler(file_path, success, message):
if success:
print(f"✓ Uploaded: {file_path}")
else:
print(f"✗ Failed: {message}")
# Could log to file, send notification, etc.
config = BackupConfig.from_env(...)
backup = DropboxBackup(config)
result = backup.run(progress_callback=progress_handler)
Backup Only Specific File Types
config = BackupConfig.from_env(
source_dir=Path.cwd(),
dropbox_path='/backups/documents',
backup_patterns=[
'*.pdf',
'*.docx',
'*.xlsx',
'reports/**/*.md',
],
exclude_patterns=[
'~$*', # Temporary Office files
'.git/*',
]
)
Basic Sync from Dropbox
from dropbox_backup_lib import DropboxSync, SyncConfig
# Sync all files from Dropbox to local
config = SyncConfig.from_env(
dropbox_path='/backups/my-project',
local_dir=Path.cwd() / 'local-copy'
)
sync = DropboxSync(config)
result = sync.run()
Pattern-Based Sync
from dropbox_backup_lib import DropboxSync, SyncConfig, ConflictResolution
# Sync only specific file types
config = SyncConfig.from_env(
dropbox_path='/backups/my-project',
local_dir=Path.cwd() / 'synced-files',
sync_patterns=['*.md', '*.py', '*.xlsx'], # Only these types
exclude_patterns=['draft_*', '*.tmp'],
conflict_resolution=ConflictResolution.TIMESTAMP
)
sync = DropboxSync(config)
result = sync.run()
Interactive Conflict Resolution
from dropbox_backup_lib import DropboxSync, SyncConfig, ConflictResolution
# Prompt user for conflicts
config = SyncConfig.from_env(
dropbox_path='/backups/my-project',
local_dir=Path.cwd(),
conflict_resolution=ConflictResolution.PROMPT
)
def handle_conflict(local_path, local_mtime, dropbox_mtime):
"""Custom conflict handler"""
print(f"\nConflict: {local_path}")
print(f" Local: {local_mtime}")
print(f" Dropbox: {dropbox_mtime}")
choice = input(" Download? (y/n): ")
return choice.lower() == 'y'
sync = DropboxSync(config)
result = sync.run(conflict_callback=handle_conflict)
Bidirectional Sync
from pathlib import Path
from dropbox_backup_lib import (
BidirectionalSync,
BidirectionalSyncConfig,
ConflictStrategy,
SyncDirection
)
# Configure bidirectional sync
config = BidirectionalSyncConfig.from_env(
local_dir=Path.cwd(),
dropbox_path='/backups/my-project',
conflict_strategy=ConflictStrategy.NEWER_WINS
)
# Custom conflict handler (optional)
def handle_conflict(path, local_mtime, remote_mtime):
"""Custom handler for when both sides changed"""
print(f"Conflict: {path}")
print(f" Local: {local_mtime}")
print(f" Remote: {remote_mtime}")
choice = input("Choose (l)ocal, (r)emote, or (s)kip: ")
if choice == 'l':
return SyncDirection.UPLOAD
elif choice == 'r':
return SyncDirection.DOWNLOAD
return SyncDirection.SKIP
# Run sync
sync = BidirectionalSync(config)
result = sync.run(
conflict_callback=handle_conflict, # Only used for PROMPT strategy
dry_run=False # Set True to preview without changes
)
print(f"Uploaded: {len(result.uploaded)}")
print(f"Downloaded: {len(result.downloaded)}")
File Patterns
The library uses glob patterns for file matching:
*.py- All Python files in the source directory**/*.py- All Python files recursivelydata/*.csv- CSV files in the data directorydocs/**/*- All files in docs and subdirectories
Default exclusion patterns:
~$*- Temporary files.git/*- Git repository.claude/*- Claude Code files*.pyc- Python bytecode__pycache__- Python cache.env*- Environment filesvenv/*- Virtual environmentnode_modules/*- Node modules
Security Best Practices
- Never commit credentials: Always use
.envfiles and add them to.gitignore - Use refresh tokens: They're more secure than access tokens and don't expire
- Limit app permissions: Only enable the Dropbox permissions you need
- Exclude sensitive files: Always exclude
.env*,*.pem,*.key, etc. - Review backup contents: Check what's being uploaded before automating
Automation
Cron (Linux/macOS)
# Edit crontab
crontab -e
# Add daily backup at 9 AM
0 9 * * * cd /path/to/project && /usr/bin/python3 backup.py >> backup.log 2>&1
Task Scheduler (Windows)
- Open Task Scheduler
- Create Basic Task
- Set trigger: Daily at 9 AM
- Set action: Run
python.exewith argumentbackup.py
GitHub Actions
Create .github/workflows/backup.yml:
name: Backup to Dropbox
on:
schedule:
- cron: '0 9 * * *' # Daily at 9 AM UTC
workflow_dispatch: # Allow manual trigger
jobs:
backup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- run: pip install dropbox-backup-lib python-dotenv
- run: python backup.py
env:
DROPBOX_APP_KEY: ${{ secrets.DROPBOX_APP_KEY }}
DROPBOX_APP_SECRET: ${{ secrets.DROPBOX_APP_SECRET }}
DROPBOX_REFRESH_TOKEN: ${{ secrets.DROPBOX_REFRESH_TOKEN }}
Troubleshooting
"Missing required environment variables"
- Make sure
.envfile exists and contains all three variables - Run
dropbox-get-tokento generate the refresh token
"Failed to connect to Dropbox"
- Check your internet connection
- Verify your credentials are correct
- Make sure your app has the required permissions
"Permission denied" or "Invalid access token"
- Go to your Dropbox app settings
- Check that permissions are enabled
- Try generating a new refresh token
Files not uploading
- Check that files match your backup patterns
- Verify files aren't excluded by exclusion patterns
- Ensure you have space in your Dropbox account
Development
Running Tests
pip install -e ".[dev]"
pytest
Code Formatting
black dropbox_backup_lib/
Type Checking
mypy dropbox_backup_lib/
License
MIT License - see LICENSE file for details
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dropbox_backup_lib-0.3.0.tar.gz.
File metadata
- Download URL: dropbox_backup_lib-0.3.0.tar.gz
- Upload date:
- Size: 35.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c5126e46f5b913a06ae607a32a87549dd4dad14a77894708883365cf0dbca113
|
|
| MD5 |
184d476b0e32abd3d1d77f04d43e2ab5
|
|
| BLAKE2b-256 |
fa83b8c99a9fb5a9d077f5af61d0ad1ec5883b68a9fb4407d1d51c41ae66caff
|
File details
Details for the file dropbox_backup_lib-0.3.0-py3-none-any.whl.
File metadata
- Download URL: dropbox_backup_lib-0.3.0-py3-none-any.whl
- Upload date:
- Size: 31.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6012c51d9e8cf4ad91b3af9ba0176a02183e44fe51400d4f4e2280606d93d015
|
|
| MD5 |
8e4fb82d78d9eb3cb99a7dd2401c1f6f
|
|
| BLAKE2b-256 |
fe3260a733e4544d45893b86cc7b3ddd1f706a2890eee5db8eb5b4b8e6a0ff8d
|