Dropbox integration into Todo.txt web application
I recently enhanced my web-based Todo.txt application by adding synchronization capabilities using the Dropbox API. This allows users to keep their todo.txt file consistent across different devices by storing and updating it in their Dropbox account. This post outlines the key steps I took to implement this feature.
Authentication: connecting to Dropbox
The first step was enabling users to connect their Dropbox accounts. I used the official Dropbox JavaScript SDK and its DropboxAuth helper for managing the OAuth 2.0 “token” flow.
- Initiating Auth: When the user clicks the “Connect” button, my app generates an authentication URL specific to my registered Dropbox app.
// Using Dropbox JS SDK
const dbxAuth = new Dropbox.DropboxAuth({ clientId: MY_CLIENT_ID });
dbxAuth.getAuthenticationUrl(MY_REDIRECT_URI, undefined, 'token')
.then(authUrl => {
window.location.href = authUrl; // Redirect user to Dropbox
})
.catch(error => { /* Handle error */ });
- Handling the Redirect: After the user approves access on Dropbox, they are redirected back to my application’s
REDIRECT_URI. The access token is included in the URL hash (#access_token=...). My application parses this token upon loading.
// On page load, check the URL hash
const urlParams = new URLSearchParams(window.location.hash.substring(1));
const accessToken = urlParams.get('access_token');
if (accessToken) {
localStorage.setItem('dropboxAccessToken', accessToken); // Store the token securely
// Clear hash from URL
window.history.replaceState({}, document.title, window.location.pathname + window.location.search);
// Initialize the main API client with the token
initializeDropboxApi(accessToken);
} else {
// Handle potential errors from hash parameters
}
- Storing the Token: The obtained access token is stored in
localStoragefor subsequent sessions. When the app loads, it checks for this stored token to automatically initialize the Dropbox client if available. Logging out simply involves removing this token fromlocalStorage.
Interacting with the Dropbox API
With an access token, the application can interact with the user’s Dropbox files using the main Dropbox client object from the SDK.
// Initialize main client (example)
let dbx = null;
if (accessToken) {
dbx = new Dropbox.Dropbox({ accessToken: accessToken });
}
Key operations include:
- Getting File Metadata: To check if the
todo.txtfile exists and get its last modified time (server_modified), I usefilesGetMetadata. This timestamp is essential for conflict detection.
async function getRemoteTimestamp() {
if (!dbx) return null;
try {
const response = await dbx.filesGetMetadata({ path: '/todo.txt' });
// Check if it's a file and return the modification date
if (response.result['.tag'] === 'file') {
return new Date(response.result.server_modified);
}
} catch (error) {
// Handle errors, especially 'path/not_found'
}
return null;
}
- Downloading the File: Fetching the content of
/todo.txtis done viafilesDownload.
async function downloadFile() {
if (!dbx) return null;
try {
const response = await dbx.filesDownload({ path: '/todo.txt' });
const fileBlob = response.result.fileBlob;
return await fileBlob.text(); // Get content as string
} catch (error) { /* Handle errors */ }
return null;
}
- Uploading the File: Saving the local
todo.txtdata involves formatting it as a string and usingfilesUploadwith theoverwritemode.
async function uploadFile(contentString) {
if (!dbx) return;
try {
await dbx.filesUpload({
path: '/todo.txt',
contents: contentString,
mode: 'overwrite', // Overwrite existing file
mute: true // Prevent Dropbox notification
});
} catch (error) { /* Handle errors */ }
}
Synchronization logic and conflict resolution
The core challenge is ensuring the local data and the Dropbox file stay consistent, especially when changes might happen on different devices between syncs. My approach relies on comparing timestamps.
-
Tracking Timestamps:
- T_{remote}: The
server_modifiedtimestamp obtained from Dropbox file metadata. - T_{local}: A timestamp stored in
localStoragewhenever the user saves changes locally within the app.
- T_{remote}: The
-
Sync Scenarios: When a sync operation is triggered (e.g., on app load or after a local change):
- If
/todo.txtdoesn’t exist on Dropbox (T_{remote} is null) but local data exists (T_{local} is not null), upload the local version. - If
/todo.txtexists but no local data/timestamp exists, download the Dropbox version. - If both exist, compare T_{local} and T_{remote}.
- If
-
Timestamp Comparison: To account for minor discrepancies, I check if the times are roughly equal using a small buffer time \epsilon (e.g., 2 seconds).
|T_{local} - T_{remote}| \le \epsilon
If this condition holds, the files are considered synchronized.
-
Handling Differences:
- If T_{local} > T_{remote} (and the difference is greater than \epsilon), the local version is newer. The application uploads the local content to Dropbox.
- If T_{remote} > T_{local} (and the difference is greater than \epsilon), the Dropbox version is newer. This indicates a potential conflict, as changes might have been made elsewhere since the last local save.
-
Conflict Resolution UI: In the T_{remote} > T_{local} case, I present a modal dialog to the user:
- It shows both the local save time and the Dropbox modification time.
- It asks the user which version to keep.
- If the user chooses “Keep Local”, the app proceeds to upload the local content, overwriting the newer version on Dropbox.
- If the user chooses “Keep Dropbox”, the app downloads the content from Dropbox and overwrites the local data.
Handling offline changes
If the user modifies their list while offline, the upload attempt fails.
- Pending Flag: I use a flag in
localStorage(dropboxUploadPending) set totruewhen an upload is attempted offline. - Online Event: When the browser comes back online (
navigator.onLinebecomes true), an event listener checks if this flag is set. - Sync Attempt: If the flag is set and the user is logged into Dropbox, the application automatically attempts the upload again, including the conflict checks described above. The flag is cleared upon successful synchronization.
UI feedback
Providing feedback is important. I implemented:
- A small indicator showing the current sync status (e.g., Idle, Syncing, Pending, Offline, Error, Not Connected).
- Changing the Dropbox connect button icon and title to reflect the login state (Connect vs. Disconnect).
- The conflict resolution modal discussed earlier.
Conclusion
Adding Dropbox synchronization involved handling authentication, using the file API for upload/download/metadata, and carefully implementing timestamp-based comparison for conflict detection and resolution. Managing offline states and providing clear UI feedback were also necessary parts of the process. This integration provides a reliable way for users to keep their todo.txt data backed up and consistent across different access points.
You can access the full source code, at the GitHub repository here.
For more insights into this topic, you can find the details here.