diff --git a/src/features/sync/lib/Sync.service.ts b/src/features/sync/lib/Sync.service.ts index f846deb..019f91a 100644 --- a/src/features/sync/lib/Sync.service.ts +++ b/src/features/sync/lib/Sync.service.ts @@ -264,6 +264,7 @@ export class SyncService extends AuthenticatedDropboxService { } } } + private async uploadFileInAssembly(dbxPath: string, uploadUrl: string, copilotApi: CopilotAPI) { logger.info('SyncService#uploadFileInAssembly :: Uploading file to Assembly', dbxPath) @@ -271,11 +272,11 @@ export class SyncService extends AuthenticatedDropboxService { const fileMetaData = await dbx.filesDownload({ path: dbxPath }) // get metadata for the files logger.info('SyncService#uploadFileInAssembly :: File metadata downloaded', dbxPath) - const downloadBody = await this.dbxClient.downloadFile( - DBX_URL_PATH.fileDownload, - dbxPath, - z.string().parse(this.connectionToken.rootNamespaceId), - ) + const downloadBody = await this.dbxClient.downloadFile({ + urlPath: DBX_URL_PATH.fileDownload, + filePath: dbxPath, + rootNamespaceId: z.string().parse(this.connectionToken.rootNamespaceId), + }) logger.info('SyncService#uploadFileInAssembly :: Found downloadBody', Boolean(downloadBody)) // upload file to assembly @@ -339,6 +340,7 @@ export class SyncService extends AuthenticatedDropboxService { portalId: this.user.portalId, assemblyFileId: file.id, } + const dbxFileInfo = await this.createAndUploadFileInDropbox(dbxRootPath, file.object, file) await this.mapFilesService.insertFileMap({ ...filePayload, @@ -423,12 +425,12 @@ export class SyncService extends AuthenticatedDropboxService { // download file from Assembly const resp = await fetch(file.downloadUrl) // upload file to dropbox - const dbxResponse = await this.dbxClient.uploadFile( - DBX_URL_PATH.fileUpload, - path, - resp.body, - z.string().parse(this.connectionToken.rootNamespaceId), - ) + const dbxResponse = await this.dbxClient.uploadFile({ + urlPath: DBX_URL_PATH.fileUpload, + filePath: path, + body: resp.body, + rootNamespaceId: z.string().parse(this.connectionToken.rootNamespaceId), + }) logger.info('SyncService#uploadFileInDropbox :: File uploaded to', path) return { dbxFileId: dbxResponse.id, diff --git a/src/features/webhook/assembly/api/webhook.controller.ts b/src/features/webhook/assembly/api/webhook.controller.ts index f1c1b61..5619d55 100644 --- a/src/features/webhook/assembly/api/webhook.controller.ts +++ b/src/features/webhook/assembly/api/webhook.controller.ts @@ -8,8 +8,10 @@ import { AssemblyWebhookService } from '@/features/webhook/assembly/lib/webhook. import { DISPATCHABLE_HANDLEABLE_EVENT } from '@/features/webhook/assembly/utils/types' import User from '@/lib/copilot/models/User.model' import logger from '@/lib/logger' +import { sleep } from '@/utils/sleep' export const handleWebhookEvent = async (req: NextRequest) => { + await sleep(800) // prevent ping-pong case of webhooks const token = req.nextUrl.searchParams.get('token') const user = await User.authenticate(token) diff --git a/src/features/webhook/assembly/lib/webhook.service.ts b/src/features/webhook/assembly/lib/webhook.service.ts index cdeb204..0eb4fbc 100644 --- a/src/features/webhook/assembly/lib/webhook.service.ts +++ b/src/features/webhook/assembly/lib/webhook.service.ts @@ -102,6 +102,9 @@ export class AssemblyWebhookService extends AuthenticatedDropboxService { logger.info('AssemblyWebhookService#handleFileCreated :: Filtered entries', filteredEntries) if (filteredEntries.length) { + // refresh dropbox access token + await this.dbxClient.dbxAuthClient.refreshAccessToken(this.connectionToken.refreshToken) + await syncAssemblyFileToDropbox.batchTrigger(filteredEntries) await this.updateLastSynced(channelSyncId) } @@ -169,6 +172,9 @@ export class AssemblyWebhookService extends AuthenticatedDropboxService { const user = this.user const connectionToken = this.connectionToken if (file) { + // refresh dropbox access token + await this.dbxClient.dbxAuthClient.refreshAccessToken(this.connectionToken.refreshToken) + const payload: AssemblyToDropboxSyncFilesPayload = { file: CopilotFileWithObjectSchema.parse(file), opts: { diff --git a/src/features/webhook/dropbox/api/webhook.controller.ts b/src/features/webhook/dropbox/api/webhook.controller.ts index 541b81f..12d19f2 100644 --- a/src/features/webhook/dropbox/api/webhook.controller.ts +++ b/src/features/webhook/dropbox/api/webhook.controller.ts @@ -3,6 +3,7 @@ import status from 'http-status' import { type NextRequest, NextResponse } from 'next/server' import env from '@/config/server.env' import { processDropboxChanges } from '@/trigger/processFileSync' +import { sleep } from '@/utils/sleep' export const handleWebhookUrlVerification = (req: NextRequest) => { try { @@ -24,6 +25,8 @@ export const handleWebhookUrlVerification = (req: NextRequest) => { } export const handleWebhookEvents = async (req: NextRequest) => { + await sleep(800) // prevent ping-pong case of webhooks + const signature = req.headers.get('X-Dropbox-Signature') if (!signature) return NextResponse.json({ error: 'Missing signature' }, { status: status.BAD_REQUEST }) diff --git a/src/lib/dropbox/DropboxAuthClient.ts b/src/lib/dropbox/DropboxAuthClient.ts index cc8f492..041f6f9 100644 --- a/src/lib/dropbox/DropboxAuthClient.ts +++ b/src/lib/dropbox/DropboxAuthClient.ts @@ -20,9 +20,10 @@ export class DropboxAuthClient { }) } - refreshAccessToken(refreshToken: string) { + // @function checkAndRefreshAccessToken() in-built function that gets a fresh access token. Refresh token never expires unless revoked manually. + async refreshAccessToken(refreshToken: string) { this.authInstance.setRefreshToken(refreshToken) - this.authInstance.checkAndRefreshAccessToken() + return await this.authInstance.checkAndRefreshAccessToken() // returns promise } async _getAuthUrl(state: string) { diff --git a/src/lib/dropbox/DropboxClient.ts b/src/lib/dropbox/DropboxClient.ts index 38069c6..7a3b0c4 100644 --- a/src/lib/dropbox/DropboxClient.ts +++ b/src/lib/dropbox/DropboxClient.ts @@ -13,7 +13,7 @@ import { dropboxArgHeader } from '@/utils/header' export class DropboxClient { protected readonly clientInstance: Dropbox - private dbxAuthClient: DropboxAuthClient + readonly dbxAuthClient: DropboxAuthClient constructor( refreshToken: string, @@ -27,17 +27,17 @@ export class DropboxClient { /** * Function returns the instance of Dropbox client after checking and refreshing (if required) the access token * @returns instance of Dropbox client - * @function checkAndRefreshAccessToken() in-built function that gets a fresh access token. Refresh token never expires unless revoked manually. */ createDropboxClient( refreshToken: string, rootNamespaceId?: string | null, type: DropboxClientTypeValue = DropboxClientType.ROOT, ): Dropbox { - this.dbxAuthClient.refreshAccessToken(refreshToken) + this.dbxAuthClient.authInstance.setRefreshToken(refreshToken) - const options: { auth: DropboxAuth; pathRoot?: string } = { + const options: { auth: DropboxAuth; refreshToken: string; pathRoot?: string } = { auth: this.dbxAuthClient.authInstance, + refreshToken, } // If we have a root namespace, set the header @@ -104,7 +104,15 @@ export class DropboxClient { return entries } - async _downloadFile(urlPath: string, filePath: string, rootNamespaceId: string) { + async _downloadFile({ + urlPath, + filePath, + rootNamespaceId, + }: { + urlPath: string + filePath: string + rootNamespaceId: string + }) { const headers = { Authorization: `Bearer ${this.dbxAuthClient.authInstance.getAccessToken()}`, 'Dropbox-API-Path-Root': dropboxArgHeader({ @@ -125,12 +133,17 @@ export class DropboxClient { * Description: this function streams the file to Dropbox. @param body is the readable stream of the file. * For the stream to work we need to add the Content-Type: 'application/octet-stream' in the headers. */ - async _uploadFile( - urlPath: string, - filePath: string, - body: NodeJS.ReadableStream | null, - rootNamespaceId: string, - ): Promise { + async _uploadFile({ + urlPath, + filePath, + body, + rootNamespaceId, + }: { + urlPath: string + filePath: string + body: NodeJS.ReadableStream | null + rootNamespaceId: string + }): Promise { const args = { path: filePath, autorename: false, diff --git a/src/trigger/processFileSync.ts b/src/trigger/processFileSync.ts index 429e75a..d3ea855 100644 --- a/src/trigger/processFileSync.ts +++ b/src/trigger/processFileSync.ts @@ -80,10 +80,8 @@ export const initiateDropboxToAssemblySync = task({ const mapFilesService = new MapFilesService(user, connectionToken) // 1. get all the files folder from dropbox - const dbxClient = new DropboxClient( - connectionToken.refreshToken, - connectionToken.rootNamespaceId, - ).getDropboxClient() + const dbx = new DropboxClient(connectionToken.refreshToken, connectionToken.rootNamespaceId) + const dbxClient = dbx.getDropboxClient() let dbxFiles = await dbxClient.filesListFolder({ path: dbxRootPath, @@ -94,6 +92,9 @@ export const initiateDropboxToAssemblySync = task({ // 2. loop over the dropbox files while (dbxFiles.result.entries.length) { + // refresh access token for every batch + await dbx.dbxAuthClient.refreshAccessToken(connectionToken.refreshToken) + const parsedDbxFiles = DropboxFileListFolderResultEntriesSchema.safeParse( dbxFiles.result.entries, ) @@ -174,6 +175,11 @@ export const handleChannelFileChanges = task({ }, run: async (payload: HandleChannelFilePayload) => { const { files, channelSyncId, dbxRootPath, assemblyChannelId, user, connectionToken } = payload + + // refresh dropbox access token + const dbxAuth = new DropboxAuthClient() + await dbxAuth.refreshAccessToken(connectionToken.refreshToken) + const mapFilesService = new MapFilesService(user, connectionToken) const mappedFiles = await mapFilesService.getAllFileMaps( and( @@ -312,16 +318,16 @@ export const initiateAssemblyToDropboxSync = task({ const { user, connectionToken, dbxRootPath, assemblyChannelId } = payload const mapFilesService = new MapFilesService(user, connectionToken) - - // refresh dropbox access token const dbxAuth = new DropboxAuthClient() - dbxAuth.refreshAccessToken(connectionToken.refreshToken) // 1. get al the files from the assembly const copilotApi = new CopilotAPI(payload.user.token) let files = await copilotApi.listFiles(payload.assemblyChannelId) while (files.data.length) { + // refresh dropbox access token for every batch + await dbxAuth.refreshAccessToken(connectionToken.refreshToken) + // 2. check and filter out all the mapped files const filteredEntries = await mapFilesService.checkAndFilterAssemblyFiles( files.data,