From 9f46e5e8ffd17e50cfe103b2c3b388b43b7c8349 Mon Sep 17 00:00:00 2001 From: "atharva.dev" Date: Wed, 29 Oct 2025 10:35:14 +0530 Subject: [PATCH] new filename to be read from headers --- .../Interfaces/FileStorageInterfaces.ts | 10 ++ .../Services/Interfaces/LoginInterfaces.ts | 1 + .../Services/Interfaces/ProtectInterfaces.ts | 3 + .../Interfaces/UnprotectInterfaces.ts | 1 + .../Services/SecloreDRMApiService.ts | 110 +++++++++++++----- .../Services/SecloreDRMFileService.ts | 6 +- nodes/SecloreProtect/Services/Utils.ts | 60 ++++++++++ .../operations/protectWithHotFolder.ts | 18 +-- nodes/SecloreProtect/operations/unprotect.ts | 17 ++- 9 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 nodes/SecloreProtect/Services/Utils.ts diff --git a/nodes/SecloreProtect/Services/Interfaces/FileStorageInterfaces.ts b/nodes/SecloreProtect/Services/Interfaces/FileStorageInterfaces.ts index de8e3a3..e82dee9 100644 --- a/nodes/SecloreProtect/Services/Interfaces/FileStorageInterfaces.ts +++ b/nodes/SecloreProtect/Services/Interfaces/FileStorageInterfaces.ts @@ -6,4 +6,14 @@ export interface IFileUploadResponse { fileSize: number; secloreFileId: string; protected: boolean; + headers?: { [key: string]: string }; +} + +export interface IFileDownloadResponse { + data: Uint8Array; + headers?: { [key: string]: string }; +} + +export interface IFileDeleteResponse { + headers?: { [key: string]: string }; } diff --git a/nodes/SecloreProtect/Services/Interfaces/LoginInterfaces.ts b/nodes/SecloreProtect/Services/Interfaces/LoginInterfaces.ts index 612ae24..ad54ca9 100644 --- a/nodes/SecloreProtect/Services/Interfaces/LoginInterfaces.ts +++ b/nodes/SecloreProtect/Services/Interfaces/LoginInterfaces.ts @@ -6,6 +6,7 @@ export interface ILoginRequest { export interface ILoginResponse { accessToken: string; refreshToken: string; + headers?: { [key: string]: string }; } export interface IRefreshTokenRequest { diff --git a/nodes/SecloreProtect/Services/Interfaces/ProtectInterfaces.ts b/nodes/SecloreProtect/Services/Interfaces/ProtectInterfaces.ts index 8090922..e9da0b3 100644 --- a/nodes/SecloreProtect/Services/Interfaces/ProtectInterfaces.ts +++ b/nodes/SecloreProtect/Services/Interfaces/ProtectInterfaces.ts @@ -14,6 +14,7 @@ export interface IProtectWithExternalRefIdRequest { export interface IProtectWithExternalRefIdResponse { fileStorageId: string; secloreFileId: string; + headers?: { [key: string]: string }; } export interface IProtectWithFileIdRequest { @@ -24,6 +25,7 @@ export interface IProtectWithFileIdRequest { export interface IProtectWithFileIdResponse { fileStorageId: string; secloreFileId: string; + headers?: { [key: string]: string }; } export interface IProtectWithHotFolderRequest { @@ -34,4 +36,5 @@ export interface IProtectWithHotFolderRequest { export interface IProtectWithHotFolderResponse { fileStorageId: string; secloreFileId: string; + headers?: { [key: string]: string }; } diff --git a/nodes/SecloreProtect/Services/Interfaces/UnprotectInterfaces.ts b/nodes/SecloreProtect/Services/Interfaces/UnprotectInterfaces.ts index cd9bedb..11a4b01 100644 --- a/nodes/SecloreProtect/Services/Interfaces/UnprotectInterfaces.ts +++ b/nodes/SecloreProtect/Services/Interfaces/UnprotectInterfaces.ts @@ -4,4 +4,5 @@ export interface IUnprotectRequest { export interface IUnprotectResponse { fileStorageId: string; + headers?: { [key: string]: string }; } diff --git a/nodes/SecloreProtect/Services/SecloreDRMApiService.ts b/nodes/SecloreProtect/Services/SecloreDRMApiService.ts index 75e0b92..e0cf464 100644 --- a/nodes/SecloreProtect/Services/SecloreDRMApiService.ts +++ b/nodes/SecloreProtect/Services/SecloreDRMApiService.ts @@ -1,6 +1,6 @@ import { IExecuteFunctions, IHttpRequestOptions, NodeApiError, LoggerProxy as Logger } from 'n8n-workflow'; import { IErrorResponse } from './Interfaces/ErrorInterfaces'; -import { IFileUploadResponse } from './Interfaces/FileStorageInterfaces'; +import { IFileUploadResponse, IFileDownloadResponse, IFileDeleteResponse } from './Interfaces/FileStorageInterfaces'; import { ILoginRequest, ILoginResponse, IRefreshTokenRequest } from './Interfaces/LoginInterfaces'; import { IProtectWithExternalRefIdRequest, @@ -95,9 +95,15 @@ export class SecloreDRMApiService { }; Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); - const response = await this.context.helpers.httpRequest(options); + const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true }); Logger.debug(who + 'Login successful', { tenantId, correlationId }); - return response as ILoginResponse; + + const loginResponse: ILoginResponse = { + ...(response.body as ILoginResponse), + headers: response.headers as { [key: string]: string } + }; + + return loginResponse; } catch (error: unknown) { Logger.error(who + 'Login failed', { error, tenantId, correlationId }); this.handleHttpError(error as NodeApiError); @@ -140,9 +146,15 @@ export class SecloreDRMApiService { }; Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); - const response = await this.context.helpers.httpRequest(options); + const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true }); Logger.debug(who + 'Token refresh successful', { correlationId }); - return response as ILoginResponse; + + const refreshResponse: ILoginResponse = { + ...(response.body as ILoginResponse), + headers: response.headers as { [key: string]: string } + }; + + return refreshResponse; } catch (error: unknown) { Logger.error(who + 'Token refresh failed', { error, correlationId }); this.handleHttpError(error as NodeApiError, { 401: 'Unauthorized' }); @@ -190,13 +202,19 @@ export class SecloreDRMApiService { }; Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); - const response = await this.context.helpers.httpRequest(options); + const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true }); Logger.debug(who + 'Protection with external ref ID successful', { fileStorageId: protectRequest.fileStorageId, - secloreFileId: (response as IProtectWithExternalRefIdResponse).secloreFileId, + secloreFileId: (response.body as IProtectWithExternalRefIdResponse).secloreFileId, correlationId }); - return response as IProtectWithExternalRefIdResponse; + + const protectResponse: IProtectWithExternalRefIdResponse = { + ...(response.body as IProtectWithExternalRefIdResponse), + headers: response.headers as { [key: string]: string } + }; + + return protectResponse; } catch (error: unknown) { Logger.error(who + 'Protection with external ref ID failed', { error, @@ -248,13 +266,19 @@ export class SecloreDRMApiService { }; Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); - const response = await this.context.helpers.httpRequest(options); + const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true }); Logger.debug(who + 'Protection with file ID successful', { existingProtectedFileId: protectRequest.existingProtectedFileId, - secloreFileId: (response as IProtectWithFileIdResponse).secloreFileId, + secloreFileId: (response.body as IProtectWithFileIdResponse).secloreFileId, correlationId }); - return response as IProtectWithFileIdResponse; + + const protectResponse: IProtectWithFileIdResponse = { + ...(response.body as IProtectWithFileIdResponse), + headers: response.headers as { [key: string]: string } + }; + + return protectResponse; } catch (error: unknown) { Logger.error(who + 'Protection with file ID failed', { error, @@ -306,13 +330,19 @@ export class SecloreDRMApiService { }; Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); - const response = await this.context.helpers.httpRequest(options); + const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true }); Logger.debug(who + 'Protection with hot folder successful', { hotfolderId: protectRequest.hotfolderId, - secloreFileId: (response as IProtectWithHotFolderResponse).secloreFileId, + secloreFileId: (response.body as IProtectWithHotFolderResponse).secloreFileId, correlationId }); - return response as IProtectWithHotFolderResponse; + + const protectResponse: IProtectWithHotFolderResponse = { + ...(response.body as IProtectWithHotFolderResponse), + headers: response.headers as { [key: string]: string } + }; + + return protectResponse; } catch (error: unknown) { Logger.error(who + 'Protection with hot folder failed', { error, @@ -364,13 +394,19 @@ export class SecloreDRMApiService { }; Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); - const response = await this.context.helpers.httpRequest(options); + const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true }); Logger.debug(who + 'Unprotection successful', { originalFileStorageId: unprotectRequest.fileStorageId, - unprotectedFileStorageId: (response as IUnprotectResponse).fileStorageId, + unprotectedFileStorageId: (response.body as IUnprotectResponse).fileStorageId, correlationId }); - return response as IUnprotectResponse; + + const unprotectResponse: IUnprotectResponse = { + ...(response.body as IUnprotectResponse), + headers: response.headers as { [key: string]: string } + }; + + return unprotectResponse; } catch (error: unknown) { Logger.error(who + 'Unprotection failed', { error, @@ -427,13 +463,19 @@ export class SecloreDRMApiService { }; Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, fileName, correlationId }); - const response = await this.context.helpers.httpRequest(options); + const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true }); Logger.debug(who + 'File upload successful', { fileName, - fileStorageId: (response as IFileUploadResponse).fileStorageId, + fileStorageId: (response.body as IFileUploadResponse).fileStorageId, correlationId }); - return response as IFileUploadResponse; + + const uploadResponse: IFileUploadResponse = { + ...(response.body as IFileUploadResponse), + headers: response.headers as { [key: string]: string } + }; + + return uploadResponse; } catch (error: unknown) { Logger.error(who + 'File upload failed', { error, @@ -452,14 +494,14 @@ export class SecloreDRMApiService { * @param fileStorageId - Storage ID of the file to be retrieved * @param accessToken - JWT access token for authorization * @param correlationId - Optional request ID for logging purpose - * @returns Promise - The downloaded file data + * @returns Promise - The downloaded file data with headers * @throws Error on authentication failure or server error */ async downloadFile( fileStorageId: string, accessToken: string, correlationId?: string, - ): Promise { + ): Promise { const who = "SecloreDRMApiService::downloadFile:: "; try { Logger.debug(who + 'Downloading file', { @@ -484,14 +526,20 @@ export class SecloreDRMApiService { }; Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, fileStorageId, correlationId }); - const response = await this.context.helpers.httpRequest(options); - const fileData = new Uint8Array(response as ArrayBuffer); + const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true }); + const fileData = new Uint8Array(response.body as ArrayBuffer); Logger.debug(who + 'File download successful', { fileStorageId, fileSize: fileData.length, correlationId }); - return fileData; + + const downloadResponse: IFileDownloadResponse = { + data: fileData, + headers: response.headers as { [key: string]: string } + }; + + return downloadResponse; } catch (error: unknown) { Logger.error(who + 'File download failed', { error, @@ -508,14 +556,14 @@ export class SecloreDRMApiService { * @param fileStorageId - Storage ID of the file to be deleted * @param accessToken - JWT access token for authorization * @param correlationId - Optional request ID for logging purpose - * @returns Promise - No response body on successful deletion + * @returns Promise - Response headers on successful deletion * @throws Error on authentication failure or server error */ async deleteFile( fileStorageId: string, accessToken: string, correlationId?: string, - ): Promise { + ): Promise { const who = "SecloreDRMApiService::deleteFile:: "; try { Logger.debug(who + 'Deleting file', { @@ -539,11 +587,17 @@ export class SecloreDRMApiService { }; Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, fileStorageId, correlationId }); - await this.context.helpers.httpRequest(options); + const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true }); Logger.debug(who + 'File deletion successful', { fileStorageId, correlationId }); + + const deleteResponse: IFileDeleteResponse = { + headers: response.headers as { [key: string]: string } + }; + + return deleteResponse; } catch (error: unknown) { Logger.error(who + 'File deletion failed', { error, diff --git a/nodes/SecloreProtect/Services/SecloreDRMFileService.ts b/nodes/SecloreProtect/Services/SecloreDRMFileService.ts index 92f0d5d..f5b62dc 100644 --- a/nodes/SecloreProtect/Services/SecloreDRMFileService.ts +++ b/nodes/SecloreProtect/Services/SecloreDRMFileService.ts @@ -1,5 +1,5 @@ import { IExecuteFunctions, LoggerProxy as Logger } from 'n8n-workflow'; -import { IFileUploadResponse } from './Interfaces/FileStorageInterfaces'; +import { IFileDownloadResponse, IFileUploadResponse } from './Interfaces/FileStorageInterfaces'; import { IProtectWithExternalRefIdRequest, IProtectWithExternalRefIdResponse, @@ -332,7 +332,7 @@ export class SecloreDRMFileService { fileStorageId: string, correlationId?: string, retryCount?: number, - ): Promise { + ): Promise { const who = "SecloreDRMFileService::downloadFile:: "; try { Logger.debug(who + 'Downloading file', { fileStorageId, correlationId }); @@ -341,7 +341,7 @@ export class SecloreDRMFileService { retryCount, correlationId, ); - Logger.info(who + 'File downloaded successfully', { fileStorageId, fileSize: result.length, correlationId }); + Logger.info(who + 'File downloaded successfully', { fileStorageId, fileSize: result.data.length, correlationId }); return result; } catch (error) { Logger.error(who + 'Download file failed', { error, fileStorageId, correlationId }); diff --git a/nodes/SecloreProtect/Services/Utils.ts b/nodes/SecloreProtect/Services/Utils.ts new file mode 100644 index 0000000..c11dd4a --- /dev/null +++ b/nodes/SecloreProtect/Services/Utils.ts @@ -0,0 +1,60 @@ +import { LoggerProxy as Logger } from 'n8n-workflow'; + +/** + * Extracts filename from Content-Disposition header + * @param headers - Response headers object + * @returns The extracted filename or null if not found + */ +export function getFileNameFromHeaders(headers?: { [key: string]: string }): string | null { + const who = "Utils::getFileNameFromHeaders:: "; + + if (!headers) { + Logger.debug(who + 'No headers provided'); + return null; + } + + // Look for content-disposition header (case-insensitive) + const contentDisposition = Object.keys(headers).find(key => + key.toLowerCase() === 'content-disposition' + ); + + if (!contentDisposition || !headers[contentDisposition]) { + Logger.debug(who + 'Content-Disposition header not found'); + return null; + } + + const headerValue = headers[contentDisposition]; + Logger.debug(who + 'Found Content-Disposition header', { headerValue }); + + // Handle different filename formats in Content-Disposition header + // Format 1: filename*=UTF-8''encoded-filename + const utf8Match = headerValue.match(/filename\*=UTF-8''([^;]+)/i); + if (utf8Match) { + try { + const decodedFilename = decodeURIComponent(utf8Match[1]); + Logger.debug(who + 'Extracted filename from UTF-8 format', { filename: decodedFilename }); + return decodedFilename; + } catch (error) { + Logger.error(who + 'Failed to decode UTF-8 filename', { error, encodedFilename: utf8Match[1] }); + } + } + + // Format 2: filename="quoted-filename" + const quotedMatch = headerValue.match(/filename="([^"]+)"/i); + if (quotedMatch) { + Logger.debug(who + 'Extracted filename from quoted format', { filename: quotedMatch[1] }); + return quotedMatch[1]; + } + + // Format 3: filename=unquoted-filename + const unquotedMatch = headerValue.match(/filename=([^;]+)/i); + if (unquotedMatch) { + const filename = unquotedMatch[1].trim(); + Logger.debug(who + 'Extracted filename from unquoted format', { filename }); + return filename; + } + + Logger.debug(who + 'Could not extract filename from Content-Disposition header', { headerValue }); + return null; +} + diff --git a/nodes/SecloreProtect/operations/protectWithHotFolder.ts b/nodes/SecloreProtect/operations/protectWithHotFolder.ts index 18aa04f..ceb7b92 100644 --- a/nodes/SecloreProtect/operations/protectWithHotFolder.ts +++ b/nodes/SecloreProtect/operations/protectWithHotFolder.ts @@ -7,6 +7,7 @@ import { } from 'n8n-workflow'; import crypto from 'node:crypto'; import { SecloreDRMFileService } from '../Services/SecloreDRMFileService'; +import { getFileNameFromHeaders } from '../Services/Utils'; /** * Deletes a file from storage with error handling (does not throw errors) @@ -70,7 +71,6 @@ async function protectFileWithHotFolder( }> { const who = "protectWithHotFolder::protectFileWithHotFolder:: "; var originalFileStorageId: string = ''; - const protectedFileName = `${fileName}.html`; try { Logger.debug(who + 'Starting protect file with hot folder operation', { fileName, fileSize: fileBuffer.length, hotfolderId, correlationId, retryCount }); @@ -120,24 +120,28 @@ async function protectFileWithHotFolder( retryCount, ); + // Try to get the actual filename from response headers, fallback to constructed name + const actualFileName = getFileNameFromHeaders(protectedFileData.headers) || fileName; + Logger.debug(who + 'Protected file downloaded successfully', { fileStorageId: protectResult.fileStorageId, - fileSize: protectedFileData.length, - fileName: protectedFileName, + fileSize: protectedFileData.data.length, + originalFileName: fileName, + actualFileName, correlationId }); const result = { - protectedFileData, + protectedFileData: protectedFileData.data, originalFileStorageId: uploadResult.fileStorageId, protectedFileStorageId: protectResult.fileStorageId, secloreFileId: protectResult.secloreFileId, - fileName: protectedFileName, - fileSize: protectedFileData.length, + fileName: actualFileName, + fileSize: protectedFileData.data.length, }; Logger.debug(who + 'Protect file with hot folder operation completed successfully', { - fileName: protectedFileName, + fileName: result.fileName, originalFileStorageId: result.originalFileStorageId, protectedFileStorageId: result.protectedFileStorageId, secloreFileId: result.secloreFileId, diff --git a/nodes/SecloreProtect/operations/unprotect.ts b/nodes/SecloreProtect/operations/unprotect.ts index a304028..8e5ecdc 100644 --- a/nodes/SecloreProtect/operations/unprotect.ts +++ b/nodes/SecloreProtect/operations/unprotect.ts @@ -7,6 +7,7 @@ import { } from 'n8n-workflow'; import crypto from 'node:crypto'; import { SecloreDRMFileService } from '../Services/SecloreDRMFileService'; +import { getFileNameFromHeaders } from '../Services/Utils'; /** * Deletes a file from storage with error handling (does not throw errors) @@ -120,19 +121,23 @@ async function unprotectFile( retryCount, ); + // Try to get the actual filename from response headers, fallback to original filename + const actualFileName = getFileNameFromHeaders(unprotectedFileData.headers) || fileName; + Logger.debug(who + 'Unprotected file downloaded successfully', { fileStorageId: unprotectResult.fileStorageId, - fileSize: unprotectedFileData.length, - fileName, + fileSize: unprotectedFileData.data.length, + originalFileName: fileName, + actualFileName: actualFileName, correlationId }); const result = { - unprotectedFileData, + unprotectedFileData: unprotectedFileData.data, originalFileStorageId: uploadResult.fileStorageId, unprotectedFileStorageId: unprotectResult.fileStorageId, - fileName, - fileSize: unprotectedFileData.length, + fileName: actualFileName, + fileSize: unprotectedFileData.data.length, }; Logger.debug(who + 'Unprotect file operation completed successfully', { @@ -235,7 +240,7 @@ export async function unprotect(this: IExecuteFunctions): Promise { }); const outputBinaryData = await this.helpers.prepareBinaryData( Buffer.from(result.unprotectedFileData), - binaryData.fileName || 'unprotected_file', + result.fileName, binaryData.mimeType, );