public with sharing class SFTPFeature extends CloudStorageBase { private static String SERVER_URL = System.Label.Ngrok; public override OperationResult listFiles(Map params) { List> resultFiles = new List>(); System.debug('Inside listFiles'); try { Map payload = (Map) params.get('payload'); String endpoint = 'https://hopelessly-upright-platypus.ngrok-free.app/api/v1/sftp'; System.debug('Endpoint: ' + endpoint); System.debug('Payload sftp: ' + JSON.serializePretty(payload)); String requestBody = JSON.serialize(payload); Map headers = new Map{ 'Content-Type' => 'application/json' }; HttpResponse res = CloudStorageUtlilty.makeCallout(requestBody,endpoint,headers,'POST'); System.debug('Status Code: ' + res.getStatusCode()); System.debug('Response Body: ' + res.getBody()); if (res.getStatusCode() == 200) { List fileList = (List) JSON.deserializeUntyped(res.getBody()); System.debug('fileList: ' + JSON.serializePretty(fileList)); // Build full tree List> tree = buildTree(fileList); // Folder filtering logic String folderId = (String) params.get('folderId'); if (String.isBlank(folderId)) { resultFiles = tree; // root } else { Map folder = findFolderByPath(tree, folderId); if (folder != null && folder.containsKey('children')) { resultFiles = (List>) folder.get('children'); } else { resultFiles = new List>(); } } } else { System.debug('SFTP call failed!'); throw new AuraHandledException('Error from SFTP service: ' + res.getBody()); } } catch (Exception e) { throw new AuraHandledException('Exception: ' + e.getMessage()); } System.debug('Final resultFiles: ' + JSON.serializePretty(resultFiles)); return OperationResult.success(resultFiles); } private List> buildTree(List items){ List> output = new List>(); for(Object obj : items){ Map node = (Map) obj; String name = (String) node.get('name'); String type = (String) node.get('type'); String path = (String) node.get('path'); Boolean isFolder = type != null && type.toLowerCase() == 'folder'; //added // Set mimeType based on type (similar to S3 logic) String mimeType; if (isFolder) { mimeType = 'doctype:folder'; } else { // Try to determine mimeType from file extension, default to attachment mimeType = getMimeTypeFromFileName(name); } Map entry = new Map{ 'name' => name, 'type' => type, 'path' => path, 'id' => path, 'fileId' => path, 'iconName' => isFolder ? 'doctype:folder' : 'doctype:attachment', 'isFolder' => String.valueOf(isFolder), 'mimeType' => mimeType // ADD THIS LINE }; // recurse children if(node.containsKey('children')){ List children = (List) node.get('children'); entry.put( 'children', children != null ? buildTree(children) : new List>() ); } else if(isFolder){ entry.put('children', new List>()); } output.add(entry); } return output; } // Add this helper method to determine mimeType from filename private String getMimeTypeFromFileName(String fileName) { if (String.isBlank(fileName)) return 'application/octet-stream'; String ext = ''; Integer lastDot = fileName.lastIndexOf('.'); if (lastDot > 0 && lastDot < fileName.length() - 1) { ext = fileName.substring(lastDot + 1).toLowerCase(); } Map mimeMap = new Map{ 'pdf' => 'application/pdf', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'xls' => 'application/vnd.ms-excel', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ppt' => 'application/vnd.ms-powerpoint', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'txt' => 'text/plain', 'csv' => 'text/csv', 'json' => 'application/json', 'xml' => 'application/xml', 'html' => 'text/html', 'htm' => 'text/html', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'bmp' => 'image/bmp', 'svg' => 'image/svg+xml', 'mp3' => 'audio/mpeg', 'mp4' => 'video/mp4', 'avi' => 'video/x-msvideo', 'mov' => 'video/quicktime', 'zip' => 'application/zip', 'rar' => 'application/x-rar-compressed', '7z' => 'application/x-7z-compressed' }; return mimeMap.containsKey(ext) ? mimeMap.get(ext) : 'application/octet-stream'; } private Map findFolderByPath(List> nodes, String path){ for(Map node : nodes){ if((String)node.get('path') == path){ return node; } if(node.containsKey('children')){ Map found =findFolderByPath((List>)node.get('children'),path); if(found != null) return found; } } return null; } //create folder public override OperationResult createFolder(Map params) { Map resultMap = new Map(); System.debug('Inside SFTP createFolder'); System.debug('Params: ' + params); try { String parentFolderId = (String) params.get('parentFolderId'); String folderName = (String) params.get('folderName'); Map payload = (Map) params.get('payload'); // Default root path if null String path = String.isBlank(parentFolderId) ? '/var/sftp/shared' : parentFolderId; // Encode folder name + path String encodedFolderName = EncodingUtil.urlEncode(folderName, 'UTF-8'); String encodedPath = EncodingUtil.urlEncode(path, 'UTF-8'); // Endpoint expected by SFTP API String endpoint = SERVER_URL + '/api/v1/sftp/folder?folderName=' + encodedFolderName + '&path=' + encodedPath; System.debug('Endpoint: ' + endpoint); // Request body expected by API String requestBody = JSON.serialize(payload); Map headers = new Map{ 'Content-Type' => 'application/json', 'accept' => '*/*' }; HttpResponse res = CloudStorageUtlilty.makeCallout( requestBody, endpoint, headers, 'POST' ); System.debug('Response code: ' + res.getStatusCode()); System.debug('Response body: ' + res.getBody()); // Handle error responses if (res.getStatusCode() != 200 && res.getStatusCode() != 201) { return OperationResult.error( 'SFTP folder creation failed: ' + res.getStatusCode() + ' - ' + res.getBody() ); } // Handle empty response safely String responseBody = res.getBody(); if (String.isNotBlank(responseBody)) { resultMap = (Map) JSON.deserializeUntyped(responseBody); } else { resultMap.put('code', res.getStatusCode()); resultMap.put('message', 'Folder created successfully'); } return OperationResult.success(resultMap); } catch (Exception e) { System.debug('SFTP create error: ' + e.getMessage()); throw new AuraHandledException( 'SFTP Folder Creation Failed: ' + e.getMessage() ); } } @AuraEnabled public static void storeSftpCredentials(Boolean isAdminCred, Map payload){ try { String username = (String) payload.get('username'); String password = (String) payload.get('password'); String host = (String) payload.get('host'); System.debug('Inside storeSftpCredentials'); System.debug('username: ' + username); System.debug('password: ' + password); System.debug('host: ' + host); String userId = SalesforceEnvironmentUtil.getUserId(); Date todayDate = System.today(); Map tokenMap = new Map(); tokenMap.put('HIC_FilesME__Auth_Provider_Name__c', 'Sftp'); tokenMap.put('ConnectedDate', todayDate); tokenMap.put('HIC_FilesME__Provider__c', 'Sftp'); tokenMap.put('HIC_FilesME__username__c', username); tokenMap.put('HIC_FilesME__Password__c', password); tokenMap.put('HIC_FilesME__Hostname__c', host); tokenMap.put('HIC_FilesME__Is_Admin_level_Credentials__c', isAdminCred); tokenMap.put('HIC_FilesME__User__c', userId); System.debug('Storing token'); OAuthTokenManager.storeToken(tokenMap); } catch (Exception e) { throw new AuraHandledException(e.getMessage()); } } private static void processItems(List items,List> resultFiles) { System.debug('Inside processItems' + JSON.serializePretty(items)); System.debug('Inside processItems' + JSON.serializePretty(resultFiles)); for (Object item : items) { Map fileMap = (Map) item; String type = (String) fileMap.get('type'); Boolean isFolder = type != null && type.toLowerCase() == 'folder'; String iconName = isFolder ? 'doctype:folder' : 'doctype:txt'; String path = (String) fileMap.get('path'); Map fileData = new Map{ 'fileId' => path, 'id' => path, 'name' => (String) fileMap.get('name'), 'type' => type, 'path' => path, 'iconName' => iconName, 'isFolder' => String.valueOf(isFolder) }; resultFiles.add(fileData); if (isFolder && fileMap.containsKey('children')) { List children = (List) fileMap.get('children'); if (children != null && !children.isEmpty()) { processItems(children, resultFiles); } } } } //to delete public override OperationResult deleteFile(Map params) { try { System.debug('>>>entered deleteFile SFTP'); String fileId = (String) params.get('fileId'); Boolean isServiceMode = (Boolean) params.get('isServiceMode'); Map payload = (Map) params.get('payload'); String endpoint = 'https://hopelessly-upright-platypus.ngrok-free.app/api/v1/sftp'; Map requestBody = new Map{ 'targetPaths' => new List{ fileId }, 'sftpConnection' => payload }; String jsonBody = JSON.serialize(requestBody); System.debug('Create Request Body: ' + jsonBody); Map headers = new Map{ 'Content-Type' => 'application/json', 'accept' => '*/*' }; HttpResponse res = CloudStorageUtlilty.makeCallout(jsonBody, endpoint, headers, 'DELETE'); System.debug('>>>res=' + res.getBody()); System.debug('>>>res.getStatusCode()=' + res.getStatusCode()); if (res.getStatusCode() == 200 || res.getStatusCode() == 204) { return OperationResult.success(res.getBody()); } else { return OperationResult.error(res.getBody()); } } catch (Exception e) { System.debug('>>>error=' + e.getMessage()); throw new AuraHandledException(e.getMessage()); } } // Rename file/folder (SFTP) public override OperationResult renameFile(Map params) { Map resultMap = new Map(); System.debug('Inside SFTP renameFile'); System.debug('Params: ' + params); try { String sourcePath = (String) params.get('fileId'); String newFileName = (String) params.get('newFileName'); Boolean overwrite = (Boolean) params.get('overwrite'); Map payload = (Map) params.get('payload'); if (overwrite == null) { overwrite = true; } System.debug('Source Path: ' + sourcePath); System.debug('New Name: ' + newFileName); // Build request body as expected by API Map requestMap = new Map{ 'sourcePath' => sourcePath, 'newName' => newFileName, 'payload' => payload, 'overwrite' => overwrite }; String requestBody = JSON.serialize(requestMap); String endpoint = SERVER_URL + '/api/v1/sftp/rename'; Map headers = new Map{ 'Content-Type' => 'application/json', 'accept' => '*/*' }; HttpResponse res = CloudStorageUtlilty.makeCallout( requestBody, endpoint, headers, 'POST' ); System.debug('Rename response: ' + res.getBody()); if (res.getStatusCode() != 200 && res.getStatusCode() != 201) { return OperationResult.error( 'SFTP rename failed: ' + res.getStatusCode() + ' - ' + res.getBody() ); } String responseBody = res.getBody(); if (String.isNotBlank(responseBody)) { resultMap = (Map) JSON.deserializeUntyped(responseBody); } else { resultMap.put('message', 'Rename successful'); } return OperationResult.success(resultMap); } catch (Exception e) { System.debug('Rename error: ' + e.getMessage()); throw new AuraHandledException( 'SFTP Rename Failed: ' + e.getMessage() ); } } //to upload @AuraEnabled(cacheable=false) public static OperationResult uploadFile(String host,String username,String password,String uploadPath,String fileName,String base64Data) { try { System.debug('>>> Inside SFTP upload'); String endpoint = 'https://hopelessly-upright-platypus.ngrok-free.app/api/v1/sftp/upload'; Map sftpConnection = new Map{ 'user' => username, 'hostname' => host, 'password' => password, 'encryptionType' => 'SFTP', 'folderName' => '' }; List items = new List{ new Map{ 'name' => fileName, 'type' => 'FILE', 'value' => base64Data } }; // Payload Map payload = new Map{ 'sftpConnection' => sftpConnection, 'uploadPath' => uploadPath, 'items' => items }; String requestBody = JSON.serialize(payload); System.debug('Upload Payload: ' + requestBody); Map headers = new Map{ 'Content-Type' => 'application/json', 'accept' => '*/*' }; HttpResponse res = CloudStorageUtlilty.makeCallout(requestBody,endpoint,headers,'POST'); System.debug('Upload Response: ' + res.getBody()); System.debug('Status: ' + res.getStatusCode()); if (res.getStatusCode() == 200 || res.getStatusCode() == 201) { return OperationResult.success(res.getBody()); } return OperationResult.error(res.getBody()); } catch (Exception e) { throw new AuraHandledException('SFTP Upload Failed: ' + e.getMessage()); } } // move file/folder public override OperationResult moveFileAndFolder(Map params) { Map resultMap = new Map(); System.debug('Inside SFTP moveFileandfolder'); System.debug('Params: ' + params); try { String sourcePath = (String) params.get('sourceKey'); System.debug('sourcepath:' + sourcePath ); String destinationPath = (String) params.get('destinationKey'); System.debug('destinationpath:' + destinationPath); Boolean overwrite = (Boolean) params.get('overwrite'); Map payload = (Map) params.get('payload'); if (overwrite == null) { overwrite = true; } System.debug('Source Path: ' + sourcePath); System.debug('Destination Path: ' + destinationPath); // API request body Map requestMap = new Map{ 'sourcePath' => sourcePath, 'destinationPath' => destinationPath, 'payload' => payload, 'overwrite' => overwrite }; String requestBody = JSON.serialize(requestMap); System.debug('Move Request Body: ' + requestBody); String endpoint = SERVER_URL + '/api/v1/sftp/move'; System.debug('endpoint: ' + endpoint); Map headers = new Map{ 'Content-Type' => 'application/json', 'accept' => '*/*' }; HttpResponse res = CloudStorageUtlilty.makeCallout(requestBody,endpoint,headers,'POST'); System.debug('Move response: ' + res.getBody()); if (res.getStatusCode() != 200 && res.getStatusCode() != 201) { return OperationResult.error('SFTP move failed: ' +res.getStatusCode() + ' - ' + res.getBody()); } String responseBody = res.getBody(); System.debug('responseBody: ' + responseBody); System.debug('res.getStatusCode(): ' + res.getStatusCode()); if (String.isNotBlank(responseBody)) { resultMap = (Map) JSON.deserializeUntyped(responseBody); } else { resultMap.put('message', 'Move successful'); } System.debug('resultMap: ' + resultMap); return OperationResult.success(resultMap); } catch (Exception e) { System.debug('Move error: ' + e.getMessage()); throw new AuraHandledException('SFTP Move Failed: ' + e.getMessage()); } } // change custom object -- linkedFolder public override OperationResult linkFiles(String recordId) { List> resultFolders = new List>(); try { List linkedFolders = [ SELECT Id,Name,CreatedDate,CreatedBy.Name,HIC_FilesME__External_Folder_ID__c, HIC_FilesME__Folder_Name__c, HIC_FilesME__Folder_URL__c, HIC_FilesME__Storage_Provider__c, HIC_FilesME__Access_Level__c, HIC_FilesME__Connected_Username__c FROM HIC_FilesME__Linked_Folder__c WHERE HIC_FilesME__Linked_Record__c = :recordId AND HIC_FilesME__Is_Active__c = true AND HIC_FilesME__Storage_Provider__c = 'SFTP' ]; System.debug('linkedFolders >>>> ' + linkedFolders); for (HIC_FilesME__Linked_Folder__c folder : linkedFolders) { Map folderData = new Map(); folderData.put('id', folder.HIC_FilesME__External_Folder_ID__c); folderData.put('recordId', folder.Id); folderData.put('name', folder.HIC_FilesME__Folder_Name__c); folderData.put('externalStorageType', folder.HIC_FilesME__Storage_Provider__c); folderData.put('accessLevel', folder.HIC_FilesME__Access_Level__c); folderData.put('createdTime', String.valueOf(folder.CreatedDate)); folderData.put('createdBy', folder.CreatedBy != null ? folder.CreatedBy.Name : '-'); folderData.put('linkedBy', folder.HIC_FilesME__Connected_Username__c); folderData.put('url', folder.HIC_FilesME__Folder_URL__c); resultFolders.add(folderData); System.debug('folderData line378 >>>> ' + folderData); } System.debug('resultFolders >>>> ' + JSON.serializePretty(resultFolders)); return OperationResult.success(resultFolders); } catch (Exception e) {FME_Logger.error(e.getMessage(),'LinkedFolderFeature' + e.getLineNumber(),'linkFiles'); return OperationResult.error('Failed to fetch linked folders. ' + e.getMessage()); } } // change custom object -- linkedFolder public override OperationResult toLinkFiles(String recordId,String folderDetails,String providerType) { try { System.debug('providerType >>>> ' + providerType); System.debug('recordId >>>> ' + recordId); System.debug('JSON.deserializeUntyped folderDetails >>>> ' + JSON.deserializeUntyped(folderDetails)); Map folderMap = (Map) JSON.deserializeUntyped(folderDetails); HIC_FilesME__Linked_Folder__c linkedFolder = new HIC_FilesME__Linked_Folder__c(); linkedFolder.HIC_FilesME__Linked_Record__c = recordId; linkedFolder.HIC_FilesME__External_Folder_ID__c = (String) folderMap.get('id'); linkedFolder.HIC_FilesME__Folder_Name__c = (String) folderMap.get('name'); //add linkedFolder.HIC_FilesME__Created__c = UserInfo.getUserId(); linkedFolder.HIC_FilesME__Created_Date__c = System.now(); linkedFolder.HIC_FilesME__Connected_Username__c = (String) folderMap.get('linkedBy'); linkedFolder.HIC_FilesME__Folder_URL__c = (String) folderMap.get('url'); linkedFolder.HIC_FilesME__Storage_Provider__c = providerType; // linkedFolder.HIC_FilesME__Access_Level__c = (String) folderMap.get('accessLevel'); linkedFolder.HIC_FilesME__Is_Active__c = true; insert linkedFolder; System.debug('linkedFolder >>>> ' + linkedFolder); return OperationResult.success(linkedFolder); } catch (Exception e) { FME_Logger.error(e.getMessage(),'LinkedFolderFeature' + e.getLineNumber(),'toLinkFiles' ); return OperationResult.error('Error linking folder: ' + e.getMessage()); } } //to unlinkfiles public override OperationResult unlinkFiles(String recordId, String filesId, Boolean deleteFromDrive) { System.debug('recordId >>>> ' + recordId); System.debug('filesId >>>> ' + filesId); System.debug('deleteFromDrive >>>> ' + deleteFromDrive); OperationResult result; try { List fileLinks = [SELECT Id FROM HIC_FilesME__Linked_Folder__c WHERE HIC_FilesME__Linked_Record__c = :recordId AND HIC_FilesME__External_Folder_ID__c = :filesId]; if (fileLinks.size() > 0) { delete fileLinks; // if (deleteFromDrive) { // deleteFile(filesId, true, null); // } result = OperationResult.success(null); } else { result = OperationResult.error('No record found'); } } catch (Exception e) { result = OperationResult.error('Error unlinking file: ' + e.getMessage()); FME_Logger.error(e.getMessage(), 'GoogleDriveFeature' + e.getLineNumber(), 'unlinkFiles'); } return result; } }