@rsweeten/dropbox-sync 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +315 -0
  2. package/dist/adapters/angular.d.ts +56 -0
  3. package/dist/adapters/angular.js +207 -0
  4. package/dist/adapters/next.d.ts +36 -0
  5. package/dist/adapters/next.js +120 -0
  6. package/dist/adapters/nuxt.d.ts +36 -0
  7. package/dist/adapters/nuxt.js +190 -0
  8. package/dist/adapters/svelte.d.ts +39 -0
  9. package/dist/adapters/svelte.js +134 -0
  10. package/dist/core/auth.d.ts +3 -0
  11. package/dist/core/auth.js +84 -0
  12. package/dist/core/client.d.ts +5 -0
  13. package/dist/core/client.js +37 -0
  14. package/dist/core/socket.d.ts +2 -0
  15. package/dist/core/socket.js +62 -0
  16. package/dist/core/sync.d.ts +3 -0
  17. package/dist/core/sync.js +340 -0
  18. package/dist/core/types.d.ts +73 -0
  19. package/dist/core/types.js +1 -0
  20. package/dist/index.d.ts +11 -0
  21. package/dist/index.js +14 -0
  22. package/examples/angular-app/dropbox-sync.service.ts +244 -0
  23. package/examples/next-app/api-routes.ts +109 -0
  24. package/examples/next-app/dropbox-client.ts +122 -0
  25. package/examples/nuxt-app/api-routes.ts +26 -0
  26. package/examples/nuxt-app/dropbox-plugin.ts +15 -0
  27. package/examples/nuxt-app/nuxt.config.ts +23 -0
  28. package/examples/svelte-app/dropbox-store.ts +174 -0
  29. package/examples/svelte-app/routes.server.ts +120 -0
  30. package/package.json +66 -0
  31. package/src/adapters/angular.ts +217 -0
  32. package/src/adapters/next.ts +155 -0
  33. package/src/adapters/nuxt.ts +270 -0
  34. package/src/adapters/svelte.ts +168 -0
  35. package/src/core/auth.ts +148 -0
  36. package/src/core/client.ts +52 -0
  37. package/src/core/socket.ts +73 -0
  38. package/src/core/sync.ts +476 -0
  39. package/src/core/types.ts +83 -0
  40. package/src/index.ts +32 -0
  41. package/tsconfig.json +16 -0
@@ -0,0 +1,476 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import { Dropbox } from 'dropbox'
4
+ import type {
5
+ DropboxCredentials,
6
+ SyncMethods,
7
+ SyncOptions,
8
+ SyncResult,
9
+ SyncProgress,
10
+ ProgressCallback,
11
+ SocketMethods,
12
+ } from './types'
13
+
14
+ // Define interface for Dropbox file entries
15
+ interface DropboxFileEntry {
16
+ '.tag': string;
17
+ path_display?: string;
18
+ [key: string]: any;
19
+ }
20
+
21
+ // Define interface for Dropbox API responses
22
+ interface DropboxListFolderResponse {
23
+ result: {
24
+ entries: DropboxFileEntry[];
25
+ has_more: boolean;
26
+ cursor: string;
27
+ };
28
+ }
29
+
30
+ interface DropboxDownloadResponse {
31
+ result: {
32
+ fileBlob?: Blob;
33
+ fileBinary?: string;
34
+ [key: string]: any;
35
+ };
36
+ }
37
+
38
+ export function createSyncMethods(
39
+ getClient: () => Dropbox,
40
+ credentials: DropboxCredentials,
41
+ socket: SocketMethods
42
+ ): SyncMethods {
43
+ let syncCancelled = false
44
+
45
+ /**
46
+ * Normalize path for consistent comparison between local and Dropbox paths
47
+ */
48
+ function normalizePath(filePath: string): string {
49
+ // Remove any leading slashes for consistency, then add a single leading slash
50
+ return '/' + filePath.replace(/^\/+/, '').toLowerCase()
51
+ }
52
+
53
+ /**
54
+ * Convert blob/buffer types to Buffer
55
+ */
56
+ async function blobToBuffer(data: any): Promise<Buffer> {
57
+ // If it's already a Buffer, return it directly
58
+ if (Buffer.isBuffer(data)) {
59
+ return data
60
+ }
61
+
62
+ // If it's a Blob, convert it to a Buffer
63
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
64
+ const arrayBuffer = await data.arrayBuffer()
65
+ return Buffer.from(arrayBuffer)
66
+ }
67
+
68
+ // If it's a plain ArrayBuffer
69
+ if (data instanceof ArrayBuffer) {
70
+ return Buffer.from(data)
71
+ }
72
+
73
+ // If it's a string
74
+ if (typeof data === 'string') {
75
+ return Buffer.from(data)
76
+ }
77
+
78
+ // If we don't know what it is, throw an error
79
+ throw new Error(`Unsupported data type: ${typeof data}`)
80
+ }
81
+
82
+ /**
83
+ * Recursively scan local directory for files to sync
84
+ */
85
+ async function scanLocalDirectory(
86
+ dir: string,
87
+ baseDir = ''
88
+ ): Promise<string[]> {
89
+ const files: string[] = []
90
+
91
+ try {
92
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
93
+
94
+ for (const entry of entries) {
95
+ const fullPath = path.join(dir, entry.name)
96
+ const relativePath = path.join(baseDir, entry.name)
97
+
98
+ if (entry.isDirectory()) {
99
+ files.push(
100
+ ...(await scanLocalDirectory(fullPath, relativePath))
101
+ )
102
+ } else if (
103
+ /\.(jpg|jpeg|png|gif|bmp|webp|svg|json)$/i.test(entry.name)
104
+ ) {
105
+ // Only include image files by default
106
+ files.push('/' + relativePath.replace(/\\/g, '/'))
107
+ }
108
+ }
109
+ } catch (error) {
110
+ console.error(`Error scanning directory ${dir}:`, error)
111
+ }
112
+
113
+ return files
114
+ }
115
+
116
+ /**
117
+ * Recursively scan Dropbox folder for files
118
+ */
119
+ async function scanDropboxFolder(
120
+ dropboxPath: string = ''
121
+ ): Promise<string[]> {
122
+ const dropbox = getClient()
123
+ const files: string[] = []
124
+
125
+ async function fetchFolderContents(path: string) {
126
+ try {
127
+ const response = await dropbox.filesListFolder({
128
+ path,
129
+ recursive: true,
130
+ }) as unknown as DropboxListFolderResponse
131
+
132
+ for (const entry of response.result.entries) {
133
+ if (
134
+ entry['.tag'] === 'file' &&
135
+ entry.path_display &&
136
+ /\.(jpg|jpeg|png|gif|bmp|webp|svg|json)$/i.test(
137
+ entry.path_display
138
+ )
139
+ ) {
140
+ files.push(entry.path_display)
141
+ }
142
+ }
143
+
144
+ if (response.result.has_more) {
145
+ await continueListing(response.result.cursor)
146
+ }
147
+ } catch (error) {
148
+ console.error('Error fetching Dropbox folder contents:', error)
149
+ throw error
150
+ }
151
+ }
152
+
153
+ async function continueListing(cursor: string) {
154
+ const response = await dropbox.filesListFolderContinue({
155
+ cursor: cursor,
156
+ }) as unknown as DropboxListFolderResponse
157
+
158
+ for (const entry of response.result.entries) {
159
+ if (
160
+ entry['.tag'] === 'file' &&
161
+ entry.path_display &&
162
+ /\.(jpg|jpeg|png|gif|bmp|webp|svg|json)$/i.test(
163
+ entry.path_display
164
+ )
165
+ ) {
166
+ files.push(entry.path_display)
167
+ }
168
+ }
169
+
170
+ if (response.result.has_more) {
171
+ await continueListing(response.result.cursor)
172
+ }
173
+ }
174
+
175
+ await fetchFolderContents(dropboxPath)
176
+ return files
177
+ }
178
+
179
+ return {
180
+ /**
181
+ * Scan local directory for files
182
+ */
183
+ async scanLocalFiles(dir?: string): Promise<string[]> {
184
+ if (!dir && typeof window === 'undefined') {
185
+ throw new Error(
186
+ 'Local directory must be provided in Node.js environment'
187
+ )
188
+ }
189
+
190
+ const localDir = dir || path.join(process.cwd(), 'public', 'img')
191
+ return scanLocalDirectory(localDir)
192
+ },
193
+
194
+ /**
195
+ * Scan Dropbox folder for files
196
+ */
197
+ async scanDropboxFiles(dir?: string): Promise<string[]> {
198
+ const dropboxDir = dir || ''
199
+ return scanDropboxFolder(dropboxDir)
200
+ },
201
+
202
+ /**
203
+ * Compare local and Dropbox files and create a sync queue
204
+ */
205
+ async createSyncQueue(
206
+ options?: Partial<SyncOptions>
207
+ ): Promise<{ uploadQueue: string[]; downloadQueue: string[] }> {
208
+ const localDir =
209
+ options?.localDir || path.join(process.cwd(), 'public', 'img')
210
+ const dropboxDir = options?.dropboxDir || ''
211
+
212
+ const localFiles = await scanLocalDirectory(localDir)
213
+ const dropboxFiles = await scanDropboxFolder(dropboxDir)
214
+
215
+ // Normalize paths for comparison
216
+ const normalizedDropboxFiles = dropboxFiles.map(normalizePath)
217
+ const normalizedLocalFiles = localFiles.map(normalizePath)
218
+
219
+ // Files to upload (in local but not in Dropbox)
220
+ const uploadQueue = localFiles.filter((localFile) => {
221
+ const normalizedLocalFile = normalizePath(localFile)
222
+ return !normalizedDropboxFiles.includes(normalizedLocalFile)
223
+ })
224
+
225
+ // Files to download (in Dropbox but not in local)
226
+ const downloadQueue = dropboxFiles.filter((dropboxFile) => {
227
+ const normalizedDropboxFile = normalizePath(dropboxFile)
228
+ return !normalizedLocalFiles.includes(normalizedDropboxFile)
229
+ })
230
+
231
+ return { uploadQueue, downloadQueue }
232
+ },
233
+
234
+ /**
235
+ * Synchronize files between local filesystem and Dropbox
236
+ */
237
+ async syncFiles(options?: Partial<SyncOptions>): Promise<SyncResult> {
238
+ const localDir =
239
+ options?.localDir || path.join(process.cwd(), 'public', 'img')
240
+ const dropboxDir = options?.dropboxDir || ''
241
+ const progressCallback = options?.progressCallback
242
+
243
+ syncCancelled = false
244
+ const result: SyncResult = {
245
+ uploaded: [],
246
+ downloaded: [],
247
+ errors: [],
248
+ }
249
+
250
+ // Report initial progress
251
+ const reportProgress = (progress: SyncProgress) => {
252
+ if (progressCallback) {
253
+ progressCallback(progress)
254
+ }
255
+
256
+ // Also emit via Socket.IO if available
257
+ if (progress.progress !== undefined && progress.message) {
258
+ socket.emit('sync:progress', {
259
+ type: progress.type || 'progress',
260
+ message: progress.message,
261
+ progress: progress.progress,
262
+ socketId: 'dropbox-sync-module',
263
+ })
264
+ }
265
+ }
266
+
267
+ reportProgress({
268
+ stage: 'init',
269
+ progress: 0,
270
+ message: 'Starting Dropbox synchronization',
271
+ })
272
+
273
+ try {
274
+ // Get the Dropbox client
275
+ const dropbox = getClient()
276
+
277
+ // Get files to sync
278
+ reportProgress({
279
+ stage: 'listing',
280
+ message: 'Analyzing files to sync...',
281
+ progress: 0,
282
+ })
283
+
284
+ const { uploadQueue, downloadQueue } =
285
+ await this.createSyncQueue({ localDir, dropboxDir })
286
+ const totalTasks = uploadQueue.length + downloadQueue.length
287
+
288
+ // Socket notification about queue
289
+ socket.emit('sync:queue', {
290
+ total: totalTasks,
291
+ totalUploads: uploadQueue.length,
292
+ totalDownloads: downloadQueue.length,
293
+ })
294
+
295
+ if (totalTasks === 0) {
296
+ reportProgress({
297
+ stage: 'complete',
298
+ progress: 100,
299
+ message: 'All files are already in sync',
300
+ })
301
+
302
+ socket.emit('sync:complete', {
303
+ message: 'All files are already in sync',
304
+ })
305
+
306
+ return result
307
+ }
308
+
309
+ let completedTasks = 0
310
+
311
+ // Upload files to Dropbox
312
+ for (const file of uploadQueue) {
313
+ if (syncCancelled) {
314
+ break
315
+ }
316
+
317
+ try {
318
+ reportProgress({
319
+ stage: 'processing',
320
+ type: 'upload',
321
+ current: completedTasks + 1,
322
+ total: totalTasks,
323
+ progress: Math.round(
324
+ (completedTasks / totalTasks) * 100
325
+ ),
326
+ message: `Uploading: ${file}`,
327
+ item: file,
328
+ })
329
+
330
+ // Upload logic
331
+ const localFilePath = path.join(
332
+ localDir,
333
+ file.replace(/^\/+/, '')
334
+ )
335
+ const dropboxFilePath = '/' + file.replace(/^\/+/, '')
336
+ const fileContent = fs.readFileSync(localFilePath)
337
+
338
+ await dropbox.filesUpload({
339
+ path: dropboxFilePath,
340
+ contents: fileContent,
341
+ mode: { '.tag': 'overwrite' },
342
+ })
343
+
344
+ result.uploaded.push(file)
345
+ completedTasks++
346
+ } catch (error) {
347
+ result.errors.push({ file, error: error as Error })
348
+ console.error(`Error uploading file ${file}:`, error)
349
+ }
350
+ }
351
+
352
+ // Download files from Dropbox
353
+ for (const file of downloadQueue) {
354
+ if (syncCancelled) {
355
+ break
356
+ }
357
+
358
+ try {
359
+ reportProgress({
360
+ stage: 'processing',
361
+ type: 'download',
362
+ current: completedTasks + 1,
363
+ total: totalTasks,
364
+ progress: Math.round(
365
+ (completedTasks / totalTasks) * 100
366
+ ),
367
+ message: `Downloading: ${file}`,
368
+ item: file,
369
+ })
370
+
371
+ // Download logic
372
+ const dropboxFilePath = file
373
+ const localRelativePath = file.replace(/^\/+/, '')
374
+ const localFilePath = path.join(
375
+ localDir,
376
+ localRelativePath
377
+ )
378
+
379
+ // Download the file from Dropbox with proper error handling
380
+ const response = await dropbox.filesDownload({
381
+ path: dropboxFilePath,
382
+ }) as unknown as DropboxDownloadResponse
383
+
384
+ // Get file content - handle different possible response formats
385
+ let fileContent: Buffer;
386
+ const responseResult = response.result;
387
+
388
+ if (responseResult.fileBlob) {
389
+ fileContent = await blobToBuffer(responseResult.fileBlob)
390
+ } else if (responseResult.fileBinary) {
391
+ fileContent = Buffer.from(
392
+ responseResult.fileBinary,
393
+ 'binary'
394
+ )
395
+ } else {
396
+ // For Node.js environment, Dropbox might return content in different properties
397
+ const contentKey = Object.keys(responseResult).find(
398
+ (key) => ['content', 'fileContent', 'file', 'data'].includes(key)
399
+ )
400
+
401
+ if (contentKey) {
402
+ fileContent = Buffer.from(responseResult[contentKey])
403
+ } else {
404
+ throw new Error(
405
+ `Could not find file content in Dropbox response for ${dropboxFilePath}`
406
+ )
407
+ }
408
+ }
409
+
410
+ // Create directory if it doesn't exist
411
+ const dirPath = path.dirname(localFilePath)
412
+ if (!fs.existsSync(dirPath)) {
413
+ fs.mkdirSync(dirPath, { recursive: true })
414
+ }
415
+
416
+ // Write the file to disk
417
+ fs.writeFileSync(localFilePath, fileContent)
418
+
419
+ result.downloaded.push(file)
420
+ completedTasks++
421
+ } catch (error) {
422
+ result.errors.push({ file, error: error as Error })
423
+ console.error(`Error downloading file ${file}:`, error)
424
+
425
+ // Emit error but continue with the next file
426
+ socket.emit('sync:error', {
427
+ message: `Error downloading ${file}: ${
428
+ (error as Error).message
429
+ }`,
430
+ continue: true,
431
+ })
432
+ }
433
+ }
434
+
435
+ // Report completion
436
+ reportProgress({
437
+ stage: 'complete',
438
+ progress: 100,
439
+ message: 'Synchronization completed successfully',
440
+ })
441
+
442
+ // Sync complete notification
443
+ socket.emit('sync:complete', {
444
+ message: 'Synchronization completed successfully',
445
+ stats: {
446
+ uploaded: result.uploaded.length,
447
+ downloaded: result.downloaded.length,
448
+ },
449
+ })
450
+ } catch (error) {
451
+ reportProgress({
452
+ stage: 'error',
453
+ message: `Error: ${(error as Error).message}`,
454
+ })
455
+
456
+ // Emit error notification
457
+ socket.emit('sync:error', {
458
+ message: (error as Error).message,
459
+ })
460
+
461
+ // Re-throw the error for handling by the caller
462
+ throw error
463
+ }
464
+
465
+ return result
466
+ },
467
+
468
+ /**
469
+ * Cancel an ongoing synchronization
470
+ */
471
+ cancelSync() {
472
+ syncCancelled = true
473
+ socket.emit('sync:cancel', {})
474
+ },
475
+ }
476
+ }
@@ -0,0 +1,83 @@
1
+ export interface DropboxCredentials {
2
+ clientId: string
3
+ clientSecret?: string
4
+ accessToken?: string
5
+ refreshToken?: string
6
+ }
7
+
8
+ export interface SyncOptions {
9
+ localDir: string
10
+ dropboxDir?: string
11
+ fileTypes?: RegExp
12
+ progressCallback?: ProgressCallback
13
+ }
14
+
15
+ export interface SyncResult {
16
+ uploaded: string[]
17
+ downloaded: string[]
18
+ errors: { file: string; error: Error }[]
19
+ }
20
+
21
+ export interface SyncProgress {
22
+ stage: 'init' | 'listing' | 'processing' | 'complete' | 'error'
23
+ current?: number
24
+ total?: number
25
+ message?: string
26
+ item?: string
27
+ progress?: number
28
+ type?: 'upload' | 'download'
29
+ }
30
+
31
+ export type ProgressCallback = (progress: SyncProgress) => void
32
+
33
+ export interface SyncActivity {
34
+ type: 'upload' | 'download'
35
+ file: string
36
+ timestamp: number
37
+ }
38
+
39
+ export interface SyncStats {
40
+ total: number
41
+ uploads: number
42
+ downloads: number
43
+ completed: number
44
+ }
45
+
46
+ export interface TokenResponse {
47
+ accessToken: string
48
+ refreshToken?: string
49
+ expiresAt?: number
50
+ }
51
+
52
+ export interface AuthMethods {
53
+ getAuthUrl(redirectUri: string, state?: string): Promise<string>
54
+ exchangeCodeForToken(
55
+ code: string,
56
+ redirectUri: string
57
+ ): Promise<TokenResponse>
58
+ refreshAccessToken(): Promise<TokenResponse>
59
+ }
60
+
61
+ export interface SyncMethods {
62
+ scanLocalFiles(dir?: string): Promise<string[]>
63
+ scanDropboxFiles(dir?: string): Promise<string[]>
64
+ createSyncQueue(
65
+ options?: Partial<SyncOptions>
66
+ ): Promise<{ uploadQueue: string[]; downloadQueue: string[] }>
67
+ syncFiles(options?: Partial<SyncOptions>): Promise<SyncResult>
68
+ cancelSync(): void
69
+ }
70
+
71
+ export interface SocketMethods {
72
+ connect(): void
73
+ disconnect(): void
74
+ on(event: string, handler: (...args: any[]) => void): void
75
+ off(event: string): void
76
+ emit(event: string, ...args: any[]): void
77
+ }
78
+
79
+ export interface DropboxSyncClient {
80
+ auth: AuthMethods
81
+ sync: SyncMethods
82
+ socket: SocketMethods
83
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ // Export all core types and functionality
2
+ export * from './core/types'
3
+ export * from './core/auth'
4
+ export * from './core/client'
5
+ export * from './core/sync'
6
+ export * from './core/socket'
7
+
8
+ // Export framework-specific adapters with renamed exports to avoid conflicts
9
+ export {
10
+ useNextDropboxSync,
11
+ createNextDropboxApiHandlers,
12
+ handleOAuthCallback as handleNextOAuthCallback,
13
+ getCredentialsFromCookies as getNextCredentialsFromCookies,
14
+ } from './adapters/next'
15
+
16
+ export {
17
+ useSvelteDropboxSync,
18
+ createSvelteKitHandlers,
19
+ getCredentialsFromCookies as getSvelteCredentialsFromCookies,
20
+ } from './adapters/svelte'
21
+
22
+ export {
23
+ useNuxtDropboxSync,
24
+ createNuxtApiHandlers,
25
+ getCredentialsFromCookies as getNuxtCredentialsFromCookies,
26
+ } from './adapters/nuxt'
27
+
28
+ export * from './adapters/angular'
29
+
30
+ // Export default client creator function
31
+ import { createDropboxSyncClient } from './core/client'
32
+ export default createDropboxSyncClient
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "declaration": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "lib": ["DOM", "ESNext"],
12
+ "skipLibCheck": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist", "examples", "**/*.test.ts"]
16
+ }