0.1.3 • Published 6 months ago

@rsweeten/dropbox-sync v0.1.3

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

Dropbox Sync Module

Test Pull Request

A reusable TypeScript module for syncing files between your application and Dropbox. Features framework-specific adapters for Next.js, SvelteKit, Nuxt, and Angular.

N.B. This NPM package is neither tested nor supported

Table of Contents

Features

  • File Synchronization: Upload local files to Dropbox and download Dropbox files to your local filesystem
  • Real-time Progress: Integrated Socket.IO support for real-time sync progress updates
  • Path Normalization: Robust path handling to ensure consistent comparison between local and remote paths
  • OAuth Authentication: Built-in OAuth flow handling for Dropbox API authentication
  • Framework Adapters: Ready-to-use integrations for Next.js, SvelteKit, and Angular
  • TypeScript Support: Fully typed API for improved developer experience

Installation

# Install the package in your project
npm install dropbox-sync
# or
yarn add dropbox-sync

You may also need to install peer dependencies:

npm install socket.io socket.io-client
# or
yarn add socket.io socket.io-client

Quick Start

Core Usage

import createDropboxSyncClient from 'dropbox-sync'

// Create a Dropbox sync client
const dropboxSync = createDropboxSyncClient({
    clientId: 'YOUR_DROPBOX_APP_KEY',
    clientSecret: 'YOUR_DROPBOX_APP_SECRET',
    accessToken: 'OPTIONAL_EXISTING_TOKEN',
    refreshToken: 'OPTIONAL_REFRESH_TOKEN',
})

// Start OAuth flow
const authUrl = await dropboxSync.auth.getAuthUrl(
    'http://localhost:3000/callback'
)
// Redirect the user to authUrl

// Exchange authorization code for tokens
const tokens = await dropboxSync.auth.exchangeCodeForToken(code, redirectUri)
// Save tokens.accessToken and tokens.refreshToken

// Sync files
const syncResult = await dropboxSync.sync.syncFiles({
    localDir: './public/img',
    dropboxDir: '/app-images',
})

console.log(`Uploaded ${syncResult.uploaded.length} files`)
console.log(`Downloaded ${syncResult.downloaded.length} files`)

Development Setup

To set up this project for local development:

# Clone the repository
git clone https://github.com/yourusername/dropbox-sync-service.git
cd dropbox-sync-service

# Install dependencies
npm install

# Run tests
npm test

# Build the package
npm run build

Architecture

The Dropbox Sync Module architecture is designed with modularity, extensibility, and framework-agnosticism in mind.

graph TD
    A[Application] --> B[Framework Adapter]
    B --> C[Core Client]
    C --> D[Auth Module]
    C --> E[Sync Module]
    C --> F[Socket Module]
    D --> G[Dropbox API]
    E --> G
    F --> H[Socket.IO]
    H --> E
    H --> A

    %% File Flow
    I[Local Files] --> E
    E --> I
    G --> E
    E --> G

Data Flow

  1. Authentication Flow:

    sequenceDiagram
        participant User
        participant App
        participant DropboxSync
        participant Dropbox
    
        User->>App: Initiate auth
        App->>DropboxSync: getAuthUrl()
        DropboxSync->>Dropbox: Request auth URL
        Dropbox-->>DropboxSync: Auth URL
        DropboxSync-->>App: Auth URL
        App-->>User: Redirect to auth URL
        User->>Dropbox: Authorize app
        Dropbox-->>App: Auth code (via redirect)
        App->>DropboxSync: exchangeCodeForToken(code)
        DropboxSync->>Dropbox: Exchange code for token
        Dropbox-->>DropboxSync: Access & refresh tokens
        DropboxSync-->>App: Tokens
  2. Sync Flow:

    sequenceDiagram
        participant App
        participant DropboxSync
        participant Socket
        participant Dropbox
        participant LocalFS
    
        App->>DropboxSync: syncFiles(options)
        DropboxSync->>LocalFS: Scan local files
        LocalFS-->>DropboxSync: Local file list
        DropboxSync->>Dropbox: List files
        Dropbox-->>DropboxSync: Dropbox file list
        DropboxSync->>DropboxSync: Create sync queue
    
        loop For each file to upload
            DropboxSync->>Dropbox: Upload file
            DropboxSync->>Socket: Emit progress
            Socket-->>App: Progress update
        end
    
        loop For each file to download
            DropboxSync->>Dropbox: Download file
            DropboxSync->>LocalFS: Write file
            DropboxSync->>Socket: Emit progress
            Socket-->>App: Progress update
        end
    
        DropboxSync-->>App: Sync results

Testing

The Dropbox Sync Module uses Jest for unit testing. Tests are organized to match the structure of the source code.

Test Structure

__tests__/              # Test utilities and mocks
src/
  core/
    __tests__/          # Core module tests
      auth.spec.ts      # Authentication tests
      client.spec.ts    # Client tests
      socket.spec.ts    # Socket tests
      sync.spec.ts      # Sync tests
  adapters/
    __tests__/          # Framework adapter tests
      angular.spec.ts   # Angular adapter tests
      next.spec.ts      # Next.js adapter tests
      nuxt.spec.ts      # Nuxt adapter tests
      svelte.spec.ts    # SvelteKit adapter tests

Running Tests

# Run all tests
npm test

# Run tests with coverage
npm test -- --coverage

# Run specific test file
npm test -- src/core/__tests__/auth.spec.ts

# Run tests in watch mode during development
npm test -- --watch

Testing Approach

  1. Unit Tests: Each module is tested in isolation with mocked dependencies
  2. Integration Tests: Tests for interactions between modules
  3. Adapter Tests: Tests for framework-specific adapters

Mock Strategy

  • Dropbox API calls are mocked using Jest mock functions
  • File system operations are mocked to avoid touching real files
  • Socket.IO is mocked to test event emission and handling

Test Coverage

We aim for high test coverage, especially for the core functionality:

ModuleCoverage
Core>90%
Auth>95%
Sync>90%
Socket>90%
Adapters>85%

Framework-Specific Usage

Next.js

// In your component
import { useNextDropboxSync } from 'dropbox-sync'

export function DropboxComponent() {
    // Initialize client
    const dropboxSync = useNextDropboxSync({
        clientId: process.env.NEXT_PUBLIC_DROPBOX_APP_KEY,
    })

    // Connect to socket for real-time updates
    useEffect(() => {
        dropboxSync.socket.connect()
        dropboxSync.socket.on('sync:progress', handleProgress)

        return () => {
            dropboxSync.socket.disconnect()
        }
    }, [])

    // Start sync
    const handleSync = () => {
        // This will trigger the server-side sync through Socket.IO
        dropboxSync.socket.emit('dropbox:sync')
    }

    return <button onClick={handleSync}>Sync with Dropbox</button>
}

// In your API route
import { createNextDropboxApiHandlers } from 'dropbox-sync'

const handlers = createNextDropboxApiHandlers()

export async function GET(request) {
    return handlers.status()
}

Nuxt.js

The Nuxt adapter leverages Nuxt's runtime config system for app settings and uses H3 (Nitro's HTTP server) utilities for handling requests and cookies. It's designed to work seamlessly with Nuxt 3's composition API and server routes.

To use this adapter in a Nuxt project, developers would:

  1. Add runtime config in their nuxt.config.ts:
export default defineNuxtConfig({
    runtimeConfig: {
        // Private keys
        dropboxAppSecret: process.env.DROPBOX_APP_SECRET,
        dropboxRedirectUri: process.env.DROPBOX_REDIRECT_URI,

        // Public keys
        public: {
            dropboxAppKey: process.env.DROPBOX_APP_KEY,
            appUrl: process.env.APP_URL || 'http://localhost:3000',
        },
    },
})
  1. Create a plugin using the example provided
  2. Set up API routes using the handlers from createNuxtApiHandlers()
// In your plugins/dropbox.ts
import { useNuxtDropboxSync } from 'dropbox-sync'
import { defineNuxtPlugin } from 'nuxt/app'

export default defineNuxtPlugin((nuxtApp) => {
  // Create the Dropbox client
  const dropboxSync = useNuxtDropboxSync()

  // Provide the client to the app
  return {
    provide: {
      dropbox: dropboxSync
    }
  }
})

// In your Vue component
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'

const { $dropbox } = useNuxtApp()
const syncProgress = ref(0)

// Handle progress updates
const handleProgress = (data) => {
  syncProgress.value = data.progress
}

// Connect to socket
onMounted(() => {
  $dropbox.socket.connect()
  $dropbox.socket.on('sync:progress', handleProgress)
})

// Clean up on unmount
onBeforeUnmount(() => {
  $dropbox.socket.disconnect()
})

// Start sync
const startSync = () => {
  $dropbox.socket.emit('dropbox:sync')
}
</script>

<template>
  <div>
    <button @click="startSync">Sync with Dropbox</button>
    <progress :value="syncProgress" max="100"></progress>
  </div>
</template>

// In your server/api/dropbox/[...].ts
import { createNuxtApiHandlers } from 'dropbox-sync'
import { defineEventHandler } from 'h3'

// Create handlers
const handlers = createNuxtApiHandlers()

export default defineEventHandler(async (event) => {
  // Handle different endpoints
  const path = event.path || ''

  if (path.endsWith('/status')) {
    return await handlers.status(event)
  }

  if (path.endsWith('/auth')) {
    return await handlers.oauthStart(event)
  }

  if (path.endsWith('/auth/callback')) {
    return await handlers.oauthCallback(event)
  }

  if (path.endsWith('/logout')) {
    return await handlers.logout(event)
  }

  return { error: 'Not found' }
})

SvelteKit

// In your store
import { useSvelteDropboxSync } from 'dropbox-sync'
import { writable } from 'svelte/store'

// Create store
export function createDropboxStore() {
    const dropboxSync = useSvelteDropboxSync({
        clientId: import.meta.env.VITE_DROPBOX_APP_KEY,
    })

    const progress = writable(0)

    // Set up socket listeners
    dropboxSync.socket.connect()
    dropboxSync.socket.on('sync:progress', (data) => {
        progress.set(data.progress)
    })

    return {
        progress: { subscribe: progress.subscribe },
        startSync: () => {
            dropboxSync.socket.emit('dropbox:sync')
        },
    }
}

Angular

// In your service
import { DropboxSyncService, getCredentialsFromEnvironment } from 'dropbox-sync'
import { environment } from '../environments/environment'

@Injectable({ providedIn: 'root' })
export class DropboxService {
    constructor(private dropboxSyncService: DropboxSyncService) {
        // Initialize the service
        this.dropboxSyncService.initialize(
            getCredentialsFromEnvironment(environment)
        )

        // Listen for socket events
        const socketEvents = this.dropboxSyncService.setupSocketListeners()
        socketEvents.subscribe((event) => console.log(event))
    }

    startSync() {
        return this.dropboxSyncService.startSync()
    }
}

API Reference

Core Client

  • createDropboxSyncClient(credentials) - Creates a new client instance

Auth Methods

  • getAuthUrl(redirectUri, state?) - Generate an OAuth authentication URL
  • exchangeCodeForToken(code, redirectUri) - Exchange authorization code for access token
  • refreshAccessToken() - Refresh an expired access token

Sync Methods

  • scanLocalFiles(dir?) - Scan local directory for files
  • scanDropboxFiles(dir?) - Scan Dropbox folder for files
  • createSyncQueue(options?) - Create upload/download queues
  • syncFiles(options?) - Synchronize files between local and Dropbox
  • cancelSync() - Cancel an ongoing synchronization

Socket Methods

  • connect() - Connect to Socket.IO
  • disconnect() - Disconnect from Socket.IO
  • on(event, handler) - Listen for an event
  • off(event) - Remove event listener
  • emit(event, ...args) - Emit an event

Socket Events

  • sync:progress - Emitted during sync with progress information
  • sync:queue - Emitted at the start of sync with queue information
  • sync:complete - Emitted when sync is complete
  • sync:error - Emitted when an error occurs during sync

Configuration Options

interface SyncOptions {
    localDir: string // Local directory path
    dropboxDir?: string // Dropbox folder path
    fileTypes?: RegExp // File types to sync (default: images and JSON)
    progressCallback?: (progress: SyncProgress) => void
}

License

MIT

0.1.3

6 months ago

0.1.2

6 months ago

0.1.1

6 months ago

0.1.0

6 months ago