Dropbox Integration Into Todo.Txt Web Application

Learning Lab
My Journey Through Books, Discoveries, and Ideas

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.

  1. 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 */ });
  1. 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
}
  1. Storing the Token: The obtained access token is stored in localStorage for 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 from localStorage.

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.txt file exists and get its last modified time (server_modified), I use filesGetMetadata. 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.txt is done via filesDownload.

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.txt data involves formatting it as a string and using filesUpload with the overwrite mode.

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.

  1. Tracking Timestamps:

    • T_{remote}: The server_modified timestamp obtained from Dropbox file metadata.
    • T_{local}: A timestamp stored in localStorage whenever the user saves changes locally within the app.
  2. Sync Scenarios: When a sync operation is triggered (e.g., on app load or after a local change):

    • If /todo.txt doesn’t exist on Dropbox (T_{remote} is null) but local data exists (T_{local} is not null), upload the local version.
    • If /todo.txt exists but no local data/timestamp exists, download the Dropbox version.
    • If both exist, compare T_{local} and T_{remote}.
  3. 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.

  1. 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.
  2. 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 to true when an upload is attempted offline.
  • Online Event: When the browser comes back online (navigator.onLine becomes 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.