Files
kdrive-n8n/src/nodes/KDrive/GenericFunctions.ts

293 lines
7.2 KiB
TypeScript

import {
IExecuteFunctions,
IDataObject,
IHttpRequestOptions,
ILoadOptionsFunctions,
INodePropertyOptions,
} from 'n8n-workflow';
import * as request from 'request';
/**
* Interface for path resolution cache
*/
interface PathCacheEntry {
fileId: string;
fileName: string;
parentId: string;
timestamp: number;
}
/**
* Path cache to store resolved paths for performance
*/
const pathCache: Record<string, PathCacheEntry> = {};
/**
* Make an API request to kDrive API
*/
export async function kdriveApiRequest(
this: IExecuteFunctions,
method: string,
endpoint: string,
body: IDataObject = {},
credentials: IDataObject,
returnFullResponse: boolean = false,
): Promise<any> {
const options: IHttpRequestOptions = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
method: method as any,
url: `https://api.infomaniak.com${endpoint}`,
body,
json: true,
};
// Add authentication
if (credentials.authentication === 'apiKey') {
options.headers!['Authorization'] = `Bearer ${credentials.apiKey}`;
}
// Handle query parameters for GET requests
if (method === 'GET' && Object.keys(body).length > 0) {
options.qs = body;
delete options.body;
}
// Handle form data for file uploads
if (endpoint.includes('/upload') && method === 'POST') {
(options as any).formData = body;
delete options.headers!['Content-Type'];
options.json = false;
}
try {
const response = await this.helpers.request!(options);
if (returnFullResponse) {
return response;
}
return response;
} catch (error: any) {
handleApiError.call(this, error);
throw error;
}
}
/**
* Handle API errors
*/
export function handleApiError(this: IExecuteFunctions, error: any): void {
let errorMessage = 'Unknown error occurred';
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
if (error.response.body) {
if (error.response.body.message) {
errorMessage = error.response.body.message;
} else if (typeof error.response.body === 'string') {
errorMessage = error.response.body;
} else if (error.response.body.error && error.response.body.error.description) {
errorMessage = error.response.body.error.description;
} else {
errorMessage = `API Error: ${error.response.statusCode} - ${error.response.statusMessage}`;
}
} else {
errorMessage = `API Error: ${error.response.statusCode} - ${error.response.statusMessage}`;
}
} else if (error.message) {
// The request was made but no response was received
errorMessage = error.message;
}
// Log the full error for debugging
console.error('kDrive API Error:', {
errorMessage,
statusCode: error.response?.statusCode,
statusMessage: error.response?.statusMessage,
responseBody: error.response?.body,
originalError: error.message
});
throw new Error(`kDrive API Error: ${errorMessage}`);
}
/**
* Clear path cache
*/
export function clearPathCache(): void {
Object.keys(pathCache).forEach(key => delete pathCache[key]);
}
/**
* Normalize path - convert to absolute path and handle path separators
*/
export function normalizePath(path: string): string {
// Replace Windows-style backslashes with forward slashes
let normalized = path.replace(/\\/g, '/');
// Remove leading/trailing slashes
normalized = normalized.replace(/^\/+|\/+$/g, '');
// Handle empty path (root)
if (!normalized) {
return 'root';
}
// Collapse multiple consecutive slashes
normalized = normalized.replace(/\/+/g, '/');
return normalized;
}
/**
* Parse path into components
*/
export function parsePath(path: string): string[] {
const normalized = normalizePath(path);
if (normalized === 'root') {
return ['root'];
}
// Split by slash and filter out empty segments
return normalized.split('/').filter(component => component.length > 0);
}
/**
* Get file info by ID
*/
export async function getFileInfoById(
this: IExecuteFunctions,
fileId: string,
driveId: string,
credentials: IDataObject
): Promise<any> {
if (fileId === 'root') {
return {
id: 'root',
name: 'root',
type: 'directory',
parent_id: null
};
}
try {
const response = await kdriveApiRequest.call(
this,
'GET',
`/3/drive/${driveId}/files/${fileId}`,
{},
credentials
);
return response;
} catch (error) {
throw new Error(`Failed to get file info for ID ${fileId}: ${error}`);
}
}
/**
* Resolve path to file ID
*/
export async function resolvePathToId(
this: IExecuteFunctions,
path: string,
driveId: string,
credentials: IDataObject
): Promise<string> {
const normalizedPath = normalizePath(path);
// Check cache first
if (pathCache[normalizedPath]) {
return pathCache[normalizedPath].fileId;
}
// Handle root path
if (normalizedPath === 'root') {
return 'root';
}
const pathComponents = parsePath(normalizedPath);
let currentId = 'root';
// Traverse path component by component
for (const component of pathComponents) {
// Get contents of current directory
const endpoint = currentId === 'root'
? `/3/drive/${driveId}/files/1/files`
: `/3/drive/${driveId}/files/${currentId}/files`;
const response = await kdriveApiRequest.call(this, 'GET', endpoint, {}, credentials);
// Find the component in the current directory
const foundItem = response.items.find((item: any) =>
item.name === component && item.type === 'directory'
);
if (!foundItem) {
throw new Error(`Path component not found: ${component} in ${currentId}`);
}
currentId = foundItem.id;
}
// Cache the resolved path
pathCache[normalizedPath] = {
fileId: currentId,
fileName: pathComponents[pathComponents.length - 1],
parentId: pathComponents.length === 1 ? 'root' :
pathCache[parsePath(normalizedPath).slice(0, -1).join('/')]?.fileId || 'root',
timestamp: Date.now()
};
return currentId;
}
/**
* Resolve file ID to path
*/
export async function resolveIdToPath(
this: IExecuteFunctions,
fileId: string,
driveId: string,
credentials: IDataObject
): Promise<string> {
if (fileId === 'root') {
return 'root';
}
// Check if we have this in cache
const cachedEntry = Object.values(pathCache).find(entry => entry.fileId === fileId);
if (cachedEntry) {
return Object.keys(pathCache).find(key => pathCache[key].fileId === fileId) || fileId;
}
// Get file info
const fileInfo = await getFileInfoById.call(this, fileId, driveId, credentials);
if (!fileInfo.parent_id) {
return fileInfo.name; // Root level file
}
// Recursively get parent path
const parentPath = await resolveIdToPath.call(this, fileInfo.parent_id, driveId, credentials);
// Construct full path
const fullPath = parentPath === 'root'
? fileInfo.name
: `${parentPath}/${fileInfo.name}`;
// Cache the result
pathCache[fullPath] = {
fileId: fileInfo.id,
fileName: fileInfo.name,
parentId: fileInfo.parent_id,
timestamp: Date.now()
};
return fullPath;
}