@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,62 @@
1
+ import { io } from 'socket.io-client';
2
+ export function createSocketMethods() {
3
+ let socket = null;
4
+ const handlers = {};
5
+ return {
6
+ /**
7
+ * Establish a Socket.IO connection
8
+ */
9
+ connect() {
10
+ if (!socket) {
11
+ // Default connection to the server's base URL
12
+ const url = globalThis?.window?.location?.origin ||
13
+ 'http://localhost:3000';
14
+ socket = io(url);
15
+ }
16
+ if (socket && !socket.connected) {
17
+ socket.connect();
18
+ }
19
+ },
20
+ /**
21
+ * Disconnect the Socket.IO connection
22
+ */
23
+ disconnect() {
24
+ if (socket && socket.connected) {
25
+ socket.disconnect();
26
+ }
27
+ },
28
+ /**
29
+ * Listen for an event on the Socket.IO connection
30
+ */
31
+ on(event, handler) {
32
+ if (!handlers[event]) {
33
+ handlers[event] = [];
34
+ }
35
+ handlers[event].push(handler);
36
+ if (socket) {
37
+ socket.on(event, handler);
38
+ }
39
+ },
40
+ /**
41
+ * Remove listeners for an event on the Socket.IO connection
42
+ */
43
+ off(event) {
44
+ if (socket && handlers[event]) {
45
+ for (const handler of handlers[event]) {
46
+ socket.off(event, handler);
47
+ }
48
+ delete handlers[event];
49
+ }
50
+ },
51
+ /**
52
+ * Emit an event on the Socket.IO connection
53
+ */
54
+ emit(event, ...args) {
55
+ if (socket) {
56
+ socket.emit(event, ...args);
57
+ return true;
58
+ }
59
+ return false;
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,3 @@
1
+ import { Dropbox } from 'dropbox';
2
+ import type { DropboxCredentials, SyncMethods, SocketMethods } from './types';
3
+ export declare function createSyncMethods(getClient: () => Dropbox, credentials: DropboxCredentials, socket: SocketMethods): SyncMethods;
@@ -0,0 +1,340 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ export function createSyncMethods(getClient, credentials, socket) {
4
+ let syncCancelled = false;
5
+ /**
6
+ * Normalize path for consistent comparison between local and Dropbox paths
7
+ */
8
+ function normalizePath(filePath) {
9
+ // Remove any leading slashes for consistency, then add a single leading slash
10
+ return '/' + filePath.replace(/^\/+/, '').toLowerCase();
11
+ }
12
+ /**
13
+ * Convert blob/buffer types to Buffer
14
+ */
15
+ async function blobToBuffer(data) {
16
+ // If it's already a Buffer, return it directly
17
+ if (Buffer.isBuffer(data)) {
18
+ return data;
19
+ }
20
+ // If it's a Blob, convert it to a Buffer
21
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
22
+ const arrayBuffer = await data.arrayBuffer();
23
+ return Buffer.from(arrayBuffer);
24
+ }
25
+ // If it's a plain ArrayBuffer
26
+ if (data instanceof ArrayBuffer) {
27
+ return Buffer.from(data);
28
+ }
29
+ // If it's a string
30
+ if (typeof data === 'string') {
31
+ return Buffer.from(data);
32
+ }
33
+ // If we don't know what it is, throw an error
34
+ throw new Error(`Unsupported data type: ${typeof data}`);
35
+ }
36
+ /**
37
+ * Recursively scan local directory for files to sync
38
+ */
39
+ async function scanLocalDirectory(dir, baseDir = '') {
40
+ const files = [];
41
+ try {
42
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
43
+ for (const entry of entries) {
44
+ const fullPath = path.join(dir, entry.name);
45
+ const relativePath = path.join(baseDir, entry.name);
46
+ if (entry.isDirectory()) {
47
+ files.push(...(await scanLocalDirectory(fullPath, relativePath)));
48
+ }
49
+ else if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|json)$/i.test(entry.name)) {
50
+ // Only include image files by default
51
+ files.push('/' + relativePath.replace(/\\/g, '/'));
52
+ }
53
+ }
54
+ }
55
+ catch (error) {
56
+ console.error(`Error scanning directory ${dir}:`, error);
57
+ }
58
+ return files;
59
+ }
60
+ /**
61
+ * Recursively scan Dropbox folder for files
62
+ */
63
+ async function scanDropboxFolder(dropboxPath = '') {
64
+ const dropbox = getClient();
65
+ const files = [];
66
+ async function fetchFolderContents(path) {
67
+ try {
68
+ const response = await dropbox.filesListFolder({
69
+ path,
70
+ recursive: true,
71
+ });
72
+ for (const entry of response.result.entries) {
73
+ if (entry['.tag'] === 'file' &&
74
+ entry.path_display &&
75
+ /\.(jpg|jpeg|png|gif|bmp|webp|svg|json)$/i.test(entry.path_display)) {
76
+ files.push(entry.path_display);
77
+ }
78
+ }
79
+ if (response.result.has_more) {
80
+ await continueListing(response.result.cursor);
81
+ }
82
+ }
83
+ catch (error) {
84
+ console.error('Error fetching Dropbox folder contents:', error);
85
+ throw error;
86
+ }
87
+ }
88
+ async function continueListing(cursor) {
89
+ const response = await dropbox.filesListFolderContinue({
90
+ cursor: cursor,
91
+ });
92
+ for (const entry of response.result.entries) {
93
+ if (entry['.tag'] === 'file' &&
94
+ entry.path_display &&
95
+ /\.(jpg|jpeg|png|gif|bmp|webp|svg|json)$/i.test(entry.path_display)) {
96
+ files.push(entry.path_display);
97
+ }
98
+ }
99
+ if (response.result.has_more) {
100
+ await continueListing(response.result.cursor);
101
+ }
102
+ }
103
+ await fetchFolderContents(dropboxPath);
104
+ return files;
105
+ }
106
+ return {
107
+ /**
108
+ * Scan local directory for files
109
+ */
110
+ async scanLocalFiles(dir) {
111
+ if (!dir && typeof window === 'undefined') {
112
+ throw new Error('Local directory must be provided in Node.js environment');
113
+ }
114
+ const localDir = dir || path.join(process.cwd(), 'public', 'img');
115
+ return scanLocalDirectory(localDir);
116
+ },
117
+ /**
118
+ * Scan Dropbox folder for files
119
+ */
120
+ async scanDropboxFiles(dir) {
121
+ const dropboxDir = dir || '';
122
+ return scanDropboxFolder(dropboxDir);
123
+ },
124
+ /**
125
+ * Compare local and Dropbox files and create a sync queue
126
+ */
127
+ async createSyncQueue(options) {
128
+ const localDir = options?.localDir || path.join(process.cwd(), 'public', 'img');
129
+ const dropboxDir = options?.dropboxDir || '';
130
+ const localFiles = await scanLocalDirectory(localDir);
131
+ const dropboxFiles = await scanDropboxFolder(dropboxDir);
132
+ // Normalize paths for comparison
133
+ const normalizedDropboxFiles = dropboxFiles.map(normalizePath);
134
+ const normalizedLocalFiles = localFiles.map(normalizePath);
135
+ // Files to upload (in local but not in Dropbox)
136
+ const uploadQueue = localFiles.filter((localFile) => {
137
+ const normalizedLocalFile = normalizePath(localFile);
138
+ return !normalizedDropboxFiles.includes(normalizedLocalFile);
139
+ });
140
+ // Files to download (in Dropbox but not in local)
141
+ const downloadQueue = dropboxFiles.filter((dropboxFile) => {
142
+ const normalizedDropboxFile = normalizePath(dropboxFile);
143
+ return !normalizedLocalFiles.includes(normalizedDropboxFile);
144
+ });
145
+ return { uploadQueue, downloadQueue };
146
+ },
147
+ /**
148
+ * Synchronize files between local filesystem and Dropbox
149
+ */
150
+ async syncFiles(options) {
151
+ const localDir = options?.localDir || path.join(process.cwd(), 'public', 'img');
152
+ const dropboxDir = options?.dropboxDir || '';
153
+ const progressCallback = options?.progressCallback;
154
+ syncCancelled = false;
155
+ const result = {
156
+ uploaded: [],
157
+ downloaded: [],
158
+ errors: [],
159
+ };
160
+ // Report initial progress
161
+ const reportProgress = (progress) => {
162
+ if (progressCallback) {
163
+ progressCallback(progress);
164
+ }
165
+ // Also emit via Socket.IO if available
166
+ if (progress.progress !== undefined && progress.message) {
167
+ socket.emit('sync:progress', {
168
+ type: progress.type || 'progress',
169
+ message: progress.message,
170
+ progress: progress.progress,
171
+ socketId: 'dropbox-sync-module',
172
+ });
173
+ }
174
+ };
175
+ reportProgress({
176
+ stage: 'init',
177
+ progress: 0,
178
+ message: 'Starting Dropbox synchronization',
179
+ });
180
+ try {
181
+ // Get the Dropbox client
182
+ const dropbox = getClient();
183
+ // Get files to sync
184
+ reportProgress({
185
+ stage: 'listing',
186
+ message: 'Analyzing files to sync...',
187
+ progress: 0,
188
+ });
189
+ const { uploadQueue, downloadQueue } = await this.createSyncQueue({ localDir, dropboxDir });
190
+ const totalTasks = uploadQueue.length + downloadQueue.length;
191
+ // Socket notification about queue
192
+ socket.emit('sync:queue', {
193
+ total: totalTasks,
194
+ totalUploads: uploadQueue.length,
195
+ totalDownloads: downloadQueue.length,
196
+ });
197
+ if (totalTasks === 0) {
198
+ reportProgress({
199
+ stage: 'complete',
200
+ progress: 100,
201
+ message: 'All files are already in sync',
202
+ });
203
+ socket.emit('sync:complete', {
204
+ message: 'All files are already in sync',
205
+ });
206
+ return result;
207
+ }
208
+ let completedTasks = 0;
209
+ // Upload files to Dropbox
210
+ for (const file of uploadQueue) {
211
+ if (syncCancelled) {
212
+ break;
213
+ }
214
+ try {
215
+ reportProgress({
216
+ stage: 'processing',
217
+ type: 'upload',
218
+ current: completedTasks + 1,
219
+ total: totalTasks,
220
+ progress: Math.round((completedTasks / totalTasks) * 100),
221
+ message: `Uploading: ${file}`,
222
+ item: file,
223
+ });
224
+ // Upload logic
225
+ const localFilePath = path.join(localDir, file.replace(/^\/+/, ''));
226
+ const dropboxFilePath = '/' + file.replace(/^\/+/, '');
227
+ const fileContent = fs.readFileSync(localFilePath);
228
+ await dropbox.filesUpload({
229
+ path: dropboxFilePath,
230
+ contents: fileContent,
231
+ mode: { '.tag': 'overwrite' },
232
+ });
233
+ result.uploaded.push(file);
234
+ completedTasks++;
235
+ }
236
+ catch (error) {
237
+ result.errors.push({ file, error: error });
238
+ console.error(`Error uploading file ${file}:`, error);
239
+ }
240
+ }
241
+ // Download files from Dropbox
242
+ for (const file of downloadQueue) {
243
+ if (syncCancelled) {
244
+ break;
245
+ }
246
+ try {
247
+ reportProgress({
248
+ stage: 'processing',
249
+ type: 'download',
250
+ current: completedTasks + 1,
251
+ total: totalTasks,
252
+ progress: Math.round((completedTasks / totalTasks) * 100),
253
+ message: `Downloading: ${file}`,
254
+ item: file,
255
+ });
256
+ // Download logic
257
+ const dropboxFilePath = file;
258
+ const localRelativePath = file.replace(/^\/+/, '');
259
+ const localFilePath = path.join(localDir, localRelativePath);
260
+ // Download the file from Dropbox with proper error handling
261
+ const response = await dropbox.filesDownload({
262
+ path: dropboxFilePath,
263
+ });
264
+ // Get file content - handle different possible response formats
265
+ let fileContent;
266
+ const responseResult = response.result;
267
+ if (responseResult.fileBlob) {
268
+ fileContent = await blobToBuffer(responseResult.fileBlob);
269
+ }
270
+ else if (responseResult.fileBinary) {
271
+ fileContent = Buffer.from(responseResult.fileBinary, 'binary');
272
+ }
273
+ else {
274
+ // For Node.js environment, Dropbox might return content in different properties
275
+ const contentKey = Object.keys(responseResult).find((key) => ['content', 'fileContent', 'file', 'data'].includes(key));
276
+ if (contentKey) {
277
+ fileContent = Buffer.from(responseResult[contentKey]);
278
+ }
279
+ else {
280
+ throw new Error(`Could not find file content in Dropbox response for ${dropboxFilePath}`);
281
+ }
282
+ }
283
+ // Create directory if it doesn't exist
284
+ const dirPath = path.dirname(localFilePath);
285
+ if (!fs.existsSync(dirPath)) {
286
+ fs.mkdirSync(dirPath, { recursive: true });
287
+ }
288
+ // Write the file to disk
289
+ fs.writeFileSync(localFilePath, fileContent);
290
+ result.downloaded.push(file);
291
+ completedTasks++;
292
+ }
293
+ catch (error) {
294
+ result.errors.push({ file, error: error });
295
+ console.error(`Error downloading file ${file}:`, error);
296
+ // Emit error but continue with the next file
297
+ socket.emit('sync:error', {
298
+ message: `Error downloading ${file}: ${error.message}`,
299
+ continue: true,
300
+ });
301
+ }
302
+ }
303
+ // Report completion
304
+ reportProgress({
305
+ stage: 'complete',
306
+ progress: 100,
307
+ message: 'Synchronization completed successfully',
308
+ });
309
+ // Sync complete notification
310
+ socket.emit('sync:complete', {
311
+ message: 'Synchronization completed successfully',
312
+ stats: {
313
+ uploaded: result.uploaded.length,
314
+ downloaded: result.downloaded.length,
315
+ },
316
+ });
317
+ }
318
+ catch (error) {
319
+ reportProgress({
320
+ stage: 'error',
321
+ message: `Error: ${error.message}`,
322
+ });
323
+ // Emit error notification
324
+ socket.emit('sync:error', {
325
+ message: error.message,
326
+ });
327
+ // Re-throw the error for handling by the caller
328
+ throw error;
329
+ }
330
+ return result;
331
+ },
332
+ /**
333
+ * Cancel an ongoing synchronization
334
+ */
335
+ cancelSync() {
336
+ syncCancelled = true;
337
+ socket.emit('sync:cancel', {});
338
+ },
339
+ };
340
+ }
@@ -0,0 +1,73 @@
1
+ export interface DropboxCredentials {
2
+ clientId: string;
3
+ clientSecret?: string;
4
+ accessToken?: string;
5
+ refreshToken?: string;
6
+ }
7
+ export interface SyncOptions {
8
+ localDir: string;
9
+ dropboxDir?: string;
10
+ fileTypes?: RegExp;
11
+ progressCallback?: ProgressCallback;
12
+ }
13
+ export interface SyncResult {
14
+ uploaded: string[];
15
+ downloaded: string[];
16
+ errors: {
17
+ file: string;
18
+ 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
+ export type ProgressCallback = (progress: SyncProgress) => void;
31
+ export interface SyncActivity {
32
+ type: 'upload' | 'download';
33
+ file: string;
34
+ timestamp: number;
35
+ }
36
+ export interface SyncStats {
37
+ total: number;
38
+ uploads: number;
39
+ downloads: number;
40
+ completed: number;
41
+ }
42
+ export interface TokenResponse {
43
+ accessToken: string;
44
+ refreshToken?: string;
45
+ expiresAt?: number;
46
+ }
47
+ export interface AuthMethods {
48
+ getAuthUrl(redirectUri: string, state?: string): Promise<string>;
49
+ exchangeCodeForToken(code: string, redirectUri: string): Promise<TokenResponse>;
50
+ refreshAccessToken(): Promise<TokenResponse>;
51
+ }
52
+ export interface SyncMethods {
53
+ scanLocalFiles(dir?: string): Promise<string[]>;
54
+ scanDropboxFiles(dir?: string): Promise<string[]>;
55
+ createSyncQueue(options?: Partial<SyncOptions>): Promise<{
56
+ uploadQueue: string[];
57
+ downloadQueue: string[];
58
+ }>;
59
+ syncFiles(options?: Partial<SyncOptions>): Promise<SyncResult>;
60
+ cancelSync(): void;
61
+ }
62
+ export interface SocketMethods {
63
+ connect(): void;
64
+ disconnect(): void;
65
+ on(event: string, handler: (...args: any[]) => void): void;
66
+ off(event: string): void;
67
+ emit(event: string, ...args: any[]): void;
68
+ }
69
+ export interface DropboxSyncClient {
70
+ auth: AuthMethods;
71
+ sync: SyncMethods;
72
+ socket: SocketMethods;
73
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ export * from './core/types';
2
+ export * from './core/auth';
3
+ export * from './core/client';
4
+ export * from './core/sync';
5
+ export * from './core/socket';
6
+ export { useNextDropboxSync, createNextDropboxApiHandlers, handleOAuthCallback as handleNextOAuthCallback, getCredentialsFromCookies as getNextCredentialsFromCookies, } from './adapters/next';
7
+ export { useSvelteDropboxSync, createSvelteKitHandlers, getCredentialsFromCookies as getSvelteCredentialsFromCookies, } from './adapters/svelte';
8
+ export { useNuxtDropboxSync, createNuxtApiHandlers, getCredentialsFromCookies as getNuxtCredentialsFromCookies, } from './adapters/nuxt';
9
+ export * from './adapters/angular';
10
+ import { createDropboxSyncClient } from './core/client';
11
+ export default createDropboxSyncClient;
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
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
+ // Export framework-specific adapters with renamed exports to avoid conflicts
8
+ export { useNextDropboxSync, createNextDropboxApiHandlers, handleOAuthCallback as handleNextOAuthCallback, getCredentialsFromCookies as getNextCredentialsFromCookies, } from './adapters/next';
9
+ export { useSvelteDropboxSync, createSvelteKitHandlers, getCredentialsFromCookies as getSvelteCredentialsFromCookies, } from './adapters/svelte';
10
+ export { useNuxtDropboxSync, createNuxtApiHandlers, getCredentialsFromCookies as getNuxtCredentialsFromCookies, } from './adapters/nuxt';
11
+ export * from './adapters/angular';
12
+ // Export default client creator function
13
+ import { createDropboxSyncClient } from './core/client';
14
+ export default createDropboxSyncClient;