293 lines
7.2 KiB
TypeScript
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;
|
|
} |