Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
type sFtpError = {
|
||||
error: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
type sFtpErrorWithCode = {
|
||||
[code: number]: sFtpError;
|
||||
}
|
||||
|
||||
const sftpErrors: sFtpErrorWithCode = {
|
||||
0: {
|
||||
error: 'OK',
|
||||
description: 'Indicates successful completion of the operation.',
|
||||
},
|
||||
1: {
|
||||
error: 'EOF',
|
||||
description: 'An attempt to read past the end-of-file was made; or, there are no more directory entries to return.',
|
||||
},
|
||||
2: {
|
||||
error: 'No such file',
|
||||
description: 'A reference was made to a file which does not exist.',
|
||||
},
|
||||
3: {
|
||||
error: 'Permission denied',
|
||||
description: 'The user does not have sufficient permissions to perform the operation.',
|
||||
},
|
||||
4: {
|
||||
error: 'Failure',
|
||||
description: 'An error occurred, but no specific error code exists to describe the failure. This error message should always have meaningful text in the error message field.',
|
||||
},
|
||||
5: {
|
||||
error: 'Bad message',
|
||||
description: 'A badly formatted packet or other SFTP protocol incompatibility was detected.',
|
||||
},
|
||||
6: {
|
||||
error: 'No connection',
|
||||
description: 'There is no connection to the server. This error may be used locally, but must not be returned by a server.',
|
||||
},
|
||||
7: {
|
||||
error: 'Connection lost',
|
||||
description: 'The connection to the server was lost. This error may be used locally, but must not be returned by a server.',
|
||||
},
|
||||
8: {
|
||||
error: 'Operation unsupported',
|
||||
description: 'An attempted operation could not be completed by the server because the server does not support the operation.',
|
||||
},
|
||||
9: {
|
||||
error: 'Invalid handle',
|
||||
description: 'The handle value was invalid.',
|
||||
},
|
||||
10: {
|
||||
error: 'No such path',
|
||||
description: 'The file path does not exist or is invalid.',
|
||||
},
|
||||
11: {
|
||||
error: 'File already exists',
|
||||
description: 'The file already exists.',
|
||||
},
|
||||
12: {
|
||||
error: 'Write protect',
|
||||
description: 'The file is on read-only media, or the media is write protected.',
|
||||
},
|
||||
13: {
|
||||
error: 'No media',
|
||||
description: 'The requested operation cannot be completed because there is no media available in the drive.',
|
||||
},
|
||||
14: {
|
||||
error: 'No space on file-system',
|
||||
description: 'The requested operation cannot be completed because there is insufficient free space on the filesystem.',
|
||||
},
|
||||
15: {
|
||||
error: 'Quota exceeded',
|
||||
description: "The operation cannot be completed because it would exceed the user’s storage quota.",
|
||||
},
|
||||
16: {
|
||||
error: 'Unknown principal',
|
||||
description: 'A principal referenced by the request (either the owner, group, or who field of an ACL), was unknown.',
|
||||
},
|
||||
17: {
|
||||
error: 'Lock conflict',
|
||||
description: 'The file could not be opened because it is locked by another process.',
|
||||
},
|
||||
18: {
|
||||
error: 'Directory not empty',
|
||||
description: 'The directory is not empty.',
|
||||
},
|
||||
19: {
|
||||
error: 'Not a directory',
|
||||
description: 'The specified file is not a directory.',
|
||||
},
|
||||
20: {
|
||||
error: 'Invalid filename',
|
||||
description: 'The filename is not valid.',
|
||||
},
|
||||
21: {
|
||||
error: 'Link loop',
|
||||
description: 'Too many symbolic links encountered or an SSH_FXF_NOFOLLOW encountered a symbolic link as the final component.',
|
||||
},
|
||||
22: {
|
||||
error: 'Cannot delete',
|
||||
description: 'The file cannot be deleted. One possible reason is that the advisory read-only attribute-bit is set.',
|
||||
},
|
||||
23: {
|
||||
error: 'Invalid parameter',
|
||||
description: 'One of the parameters was out of range or the parameters specified cannot be used together.',
|
||||
},
|
||||
24: {
|
||||
error: 'File is a directory',
|
||||
description: 'The specified file was a directory in a context where a directory cannot be used.',
|
||||
},
|
||||
25: {
|
||||
error: 'Range lock conflict',
|
||||
description: 'A read or write operation failed because another process’s mandatory byte-range lock overlaps with the request.',
|
||||
},
|
||||
26: {
|
||||
error: 'Range lock refused',
|
||||
description: 'A request for a byte range lock was refused.',
|
||||
},
|
||||
27: {
|
||||
error: 'Delete pending',
|
||||
description: 'An operation was attempted on a file for which a delete operation is pending.',
|
||||
},
|
||||
28: {
|
||||
error: 'File corrupt',
|
||||
description: 'The file is corrupt; a filesystem integrity check should be run.',
|
||||
},
|
||||
29: {
|
||||
error: 'Owner invalid',
|
||||
description: 'The principal specified cannot be assigned as an owner of a file.',
|
||||
},
|
||||
30: {
|
||||
error: 'Group invalid',
|
||||
description: 'The principal specified cannot be assigned as the primary group of a file.',
|
||||
},
|
||||
31: {
|
||||
error: 'No matching byte range lock',
|
||||
description: 'The requested operation could not be completed because the specified byte range lock has not been granted.',
|
||||
},
|
||||
};
|
||||
|
||||
export function getSftpError(code: number): sFtpError {
|
||||
const error = sftpErrors[code];
|
||||
return error ?? { error: 'Unknown error code' };
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import Client from 'ssh2-sftp-client';
|
||||
import { Client as FTPClient, FTPError } from 'basic-ftp';
|
||||
import { endClient, getClient, getProtocolBackwardCompatibility, sftpAuth } from '../..';
|
||||
import { Readable } from 'stream';
|
||||
import { getSftpError } from './common';
|
||||
|
||||
async function createFileWithSFTP(client: Client, fileName: string, fileContent: string) {
|
||||
const remotePathExists = await client.exists(fileName);
|
||||
if (!remotePathExists) {
|
||||
// Extract the directory path from the fileName
|
||||
const remoteDirectory = fileName.substring(0, fileName.lastIndexOf('/'));
|
||||
// Create the directory if it doesn't exist
|
||||
await client.mkdir(remoteDirectory, true);
|
||||
}
|
||||
await client.put(Buffer.from(fileContent), fileName);
|
||||
}
|
||||
|
||||
async function createFileWithFTP(client: FTPClient, fileName: string, fileContent: string) {
|
||||
// Extract the directory path from the fileName
|
||||
const remoteDirectory = fileName.substring(0, fileName.lastIndexOf('/'));
|
||||
// Create the directory if it doesn't exist
|
||||
await client.ensureDir(remoteDirectory);
|
||||
// Upload the file content
|
||||
const buffer = Buffer.from(fileContent);
|
||||
await client.uploadFrom(Readable.from(buffer), fileName);
|
||||
}
|
||||
|
||||
export const createFile = createAction({
|
||||
auth: sftpAuth,
|
||||
name: 'create_file',
|
||||
displayName: 'Create File from Text',
|
||||
description: 'Create a new file in the given path',
|
||||
props: {
|
||||
fileName: Property.ShortText({
|
||||
displayName: 'File Path',
|
||||
required: true,
|
||||
}),
|
||||
fileContent: Property.LongText({
|
||||
displayName: 'File content',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const fileName = context.propsValue['fileName'];
|
||||
const fileContent = context.propsValue['fileContent'];
|
||||
const protocolBackwardCompatibility = await getProtocolBackwardCompatibility(context.auth.props.protocol);
|
||||
const client = await getClient(context.auth.props);
|
||||
|
||||
try {
|
||||
switch (protocolBackwardCompatibility) {
|
||||
case 'ftps':
|
||||
case 'ftp':
|
||||
await createFileWithFTP(client as FTPClient, fileName, fileContent);
|
||||
break;
|
||||
default:
|
||||
case 'sftp':
|
||||
await createFileWithSFTP(client as Client, fileName, fileContent);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof FTPError) {
|
||||
console.error(getSftpError(err.code));
|
||||
return {
|
||||
status: 'error',
|
||||
error: getSftpError(err.code),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'error',
|
||||
error: err
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await endClient(client, context.auth.props.protocol);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { endClient, getClient, getProtocolBackwardCompatibility, sftpAuth } from '../../index';
|
||||
import { Property, createAction } from '@activepieces/pieces-framework';
|
||||
import Client from 'ssh2-sftp-client';
|
||||
import { Client as FTPClient, FTPError } from 'basic-ftp';
|
||||
import { getSftpError } from './common';
|
||||
|
||||
export const createFolderAction = createAction({
|
||||
auth: sftpAuth,
|
||||
name: 'createFolder',
|
||||
displayName: 'Create Folder',
|
||||
description: 'Creates a folder at given path.',
|
||||
props: {
|
||||
folderPath: Property.ShortText({
|
||||
displayName: 'Folder Path',
|
||||
required: true,
|
||||
description: 'The new folder path e.g. `./myfolder`. For FTP/FTPS, it will create nested folders if necessary.',
|
||||
}),
|
||||
recursive: Property.Checkbox({
|
||||
displayName: 'Recursive',
|
||||
defaultValue: false,
|
||||
required: false,
|
||||
description: 'For SFTP only: Create parent directories if they do not exist',
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const client = await getClient(context.auth.props);
|
||||
const directoryPath = context.propsValue.folderPath;
|
||||
const recursive = context.propsValue.recursive ?? false;
|
||||
const protocolBackwardCompatibility = await getProtocolBackwardCompatibility(context.auth.props.protocol);
|
||||
try {
|
||||
switch (protocolBackwardCompatibility) {
|
||||
case 'ftps':
|
||||
case 'ftp':
|
||||
await (client as FTPClient).ensureDir(directoryPath);
|
||||
break;
|
||||
default:
|
||||
case 'sftp':
|
||||
await (client as Client).mkdir(directoryPath, recursive);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof FTPError) {
|
||||
console.error(getSftpError(err.code));
|
||||
return {
|
||||
status: 'error',
|
||||
error: getSftpError(err.code),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'error',
|
||||
error: err
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await endClient(client, context.auth.props.protocol);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { endClient, getClient, getProtocolBackwardCompatibility, sftpAuth } from '../..';
|
||||
import { Client as FTPClient, FTPError } from 'basic-ftp';
|
||||
import Client from 'ssh2-sftp-client';
|
||||
import { getSftpError } from './common';
|
||||
|
||||
async function deleteFileFromFTP(client: FTPClient, filePath: string) {
|
||||
await client.remove(filePath);
|
||||
}
|
||||
|
||||
async function deleteFileFromSFTP(client: Client, filePath: string) {
|
||||
await client.delete(filePath);
|
||||
}
|
||||
|
||||
export const deleteFileAction = createAction({
|
||||
auth: sftpAuth,
|
||||
name: 'deleteFile',
|
||||
displayName: 'Delete file',
|
||||
description: 'Deletes a file at given path.',
|
||||
props: {
|
||||
filePath: Property.ShortText({
|
||||
displayName: 'File Path',
|
||||
required: true,
|
||||
description: 'The path of the file to delete e.g. `./myfolder/test.mp3`',
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const client = await getClient(context.auth.props);
|
||||
const filePath = context.propsValue.filePath;
|
||||
const protocolBackwardCompatibility = await getProtocolBackwardCompatibility(context.auth.props.protocol);
|
||||
try {
|
||||
switch (protocolBackwardCompatibility) {
|
||||
case 'ftps':
|
||||
case 'ftp':
|
||||
await deleteFileFromFTP(client as FTPClient, filePath);
|
||||
break;
|
||||
default:
|
||||
case 'sftp':
|
||||
await deleteFileFromSFTP(client as Client, filePath);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof FTPError) {
|
||||
console.error(getSftpError(err.code));
|
||||
return {
|
||||
status: 'error',
|
||||
error: getSftpError(err.code),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'error',
|
||||
error: err
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await endClient(client, context.auth.props.protocol);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import Client from 'ssh2-sftp-client';
|
||||
import { Client as FTPClient, FTPError } from 'basic-ftp';
|
||||
import { endClient, getClient, getProtocolBackwardCompatibility, sftpAuth } from '../..';
|
||||
import { getSftpError } from './common';
|
||||
|
||||
async function deleteFolderFTP(client: FTPClient, directoryPath: string, recursive: boolean) {
|
||||
if (recursive) {
|
||||
await client.removeDir(directoryPath);
|
||||
} else {
|
||||
await client.removeEmptyDir(directoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFolderSFTP(client: Client, directoryPath: string, recursive: boolean) {
|
||||
await client.rmdir(directoryPath, recursive);
|
||||
}
|
||||
|
||||
export const deleteFolderAction = createAction({
|
||||
auth: sftpAuth,
|
||||
name: 'deleteFolder',
|
||||
displayName: 'Delete Folder',
|
||||
description: 'Deletes an existing folder at given path.',
|
||||
props: {
|
||||
folderPath: Property.ShortText({
|
||||
displayName: 'Folder Path',
|
||||
required: true,
|
||||
description: 'The path of the folder to delete e.g. `./myfolder`',
|
||||
}),
|
||||
recursive: Property.Checkbox({
|
||||
displayName: 'Recursive',
|
||||
defaultValue: false,
|
||||
required: false,
|
||||
description:
|
||||
'Enable this option to delete the folder and all its contents, including subfolders and files.',
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const client = await getClient(context.auth.props);
|
||||
const directoryPath = context.propsValue.folderPath;
|
||||
const recursive = context.propsValue.recursive ?? false;
|
||||
const protocolBackwardCompatibility = await getProtocolBackwardCompatibility(context.auth.props.protocol);
|
||||
try {
|
||||
switch (protocolBackwardCompatibility) {
|
||||
case 'ftps':
|
||||
case 'ftp':
|
||||
await deleteFolderFTP(client as FTPClient, directoryPath, recursive);
|
||||
break;
|
||||
default:
|
||||
case 'sftp':
|
||||
await deleteFolderSFTP(client as Client, directoryPath, recursive);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof FTPError) {
|
||||
console.error(getSftpError(err.code));
|
||||
return {
|
||||
status: 'error',
|
||||
error: getSftpError(err.code),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'error',
|
||||
error: err
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await endClient(client, context.auth.props.protocol);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { endClient, sftpAuth } from '../../index';
|
||||
import { Property, createAction } from '@activepieces/pieces-framework';
|
||||
import Client from 'ssh2-sftp-client';
|
||||
import { Client as FTPClient, FTPError } from 'basic-ftp';
|
||||
import { getClient, getProtocolBackwardCompatibility } from '../..';
|
||||
import { getSftpError } from './common';
|
||||
import { unknown } from 'zod';
|
||||
|
||||
async function listSFTP(client: Client, directoryPath: string) {
|
||||
const contents = await client.list(directoryPath);
|
||||
await client.end();
|
||||
return contents;
|
||||
}
|
||||
|
||||
async function listFTP(client: FTPClient, directoryPath: string) {
|
||||
const contents = await client.list(directoryPath);
|
||||
return contents.map(item => ({
|
||||
type: item.type === 1 ? 'd' : '-',
|
||||
name: item.name,
|
||||
size: item.size,
|
||||
modifyTime: item.modifiedAt,
|
||||
accessTime: item.modifiedAt, // FTP doesn't provide access time
|
||||
rights: {
|
||||
user: item.permissions || '',
|
||||
group: '',
|
||||
other: ''
|
||||
},
|
||||
owner: '',
|
||||
group: ''
|
||||
}));
|
||||
}
|
||||
|
||||
export const listFolderContentsAction = createAction({
|
||||
auth: sftpAuth,
|
||||
name: 'listFolderContents',
|
||||
displayName: 'List Folder Contents',
|
||||
description: 'Lists the contents of a given folder.',
|
||||
props: {
|
||||
directoryPath: Property.ShortText({
|
||||
displayName: 'Directory Path',
|
||||
required: true,
|
||||
description: 'The path of the folder to list e.g. `./myfolder`',
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const client = await getClient(context.auth.props);
|
||||
const directoryPath = context.propsValue.directoryPath;
|
||||
const protocolBackwardCompatibility = await getProtocolBackwardCompatibility(context.auth.props.protocol);
|
||||
try {
|
||||
let contents;
|
||||
switch (protocolBackwardCompatibility) {
|
||||
case 'ftps':
|
||||
case 'ftp':
|
||||
contents = await listFTP(client as FTPClient, directoryPath);
|
||||
break;
|
||||
default:
|
||||
case 'sftp':
|
||||
contents = await listSFTP(client as Client, directoryPath);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
contents: contents,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof FTPError) {
|
||||
console.error(getSftpError(err.code));
|
||||
return {
|
||||
status: 'error',
|
||||
content: null,
|
||||
error: getSftpError(err.code),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'error',
|
||||
content: null,
|
||||
error: err
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await endClient(client, context.auth.props.protocol);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { endClient, sftpAuth } from '../../index';
|
||||
import { Property, createAction } from '@activepieces/pieces-framework';
|
||||
import Client from 'ssh2-sftp-client';
|
||||
import { Client as FTPClient, FTPError } from 'basic-ftp';
|
||||
import { getClient, getProtocolBackwardCompatibility } from '../..';
|
||||
import { Writable } from 'stream';
|
||||
import { getSftpError } from './common';
|
||||
|
||||
async function readFTP(client: FTPClient, filePath: string) {
|
||||
const chunks: Buffer[] = [];
|
||||
const writeStream = new Writable({
|
||||
write(chunk: Buffer, _encoding: string, callback: () => void) {
|
||||
chunks.push(chunk);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
await client.downloadTo(writeStream, filePath);
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
async function readSFTP(client: Client, filePath: string) {
|
||||
const fileContent = await client.get(filePath);
|
||||
await client.end();
|
||||
return fileContent as Buffer;
|
||||
}
|
||||
|
||||
export const readFileContent = createAction({
|
||||
auth: sftpAuth,
|
||||
name: 'read_file_content',
|
||||
displayName: 'Read File Content',
|
||||
description: 'Read the content of a file.',
|
||||
props: {
|
||||
filePath: Property.ShortText({
|
||||
displayName: 'File Path',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const client = await getClient(context.auth.props);
|
||||
const filePath = context.propsValue['filePath'];
|
||||
const fileName = filePath.split('/').pop() ?? filePath;
|
||||
const protocolBackwardCompatibility = await getProtocolBackwardCompatibility(context.auth.props.protocol);
|
||||
try {
|
||||
let fileContent: Buffer;
|
||||
switch (protocolBackwardCompatibility) {
|
||||
case 'ftps':
|
||||
case 'ftp':
|
||||
fileContent = await readFTP(client as FTPClient, filePath);
|
||||
break;
|
||||
default:
|
||||
case 'sftp':
|
||||
fileContent = await readSFTP(client as Client, filePath);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
file: await context.files.write({
|
||||
fileName: fileName,
|
||||
data: fileContent,
|
||||
}),
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof FTPError) {
|
||||
console.error(getSftpError(err.code));
|
||||
return {
|
||||
status: 'error',
|
||||
content: null,
|
||||
error: getSftpError(err.code),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'error',
|
||||
content: null,
|
||||
error: err
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await endClient(client, context.auth.props.protocol);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { endClient, sftpAuth } from '../../index';
|
||||
import { Property, createAction } from '@activepieces/pieces-framework';
|
||||
import Client from 'ssh2-sftp-client';
|
||||
import { Client as FTPClient, FTPError } from 'basic-ftp';
|
||||
import { getClient, getProtocolBackwardCompatibility } from '../..';
|
||||
import { MarkdownVariant } from '@activepieces/shared';
|
||||
import { getSftpError } from './common';
|
||||
|
||||
async function renameFTP(client: FTPClient, oldPath: string, newPath: string) {
|
||||
await client.rename(oldPath, newPath);
|
||||
}
|
||||
|
||||
async function renameSFTP(client: Client, oldPath: string, newPath: string) {
|
||||
await client.rename(oldPath, newPath);
|
||||
await client.end();
|
||||
}
|
||||
|
||||
export const renameFileOrFolderAction = createAction({
|
||||
auth: sftpAuth,
|
||||
name: 'renameFileOrFolder',
|
||||
displayName: 'Rename File or Folder',
|
||||
description: 'Renames a file or folder at given path.',
|
||||
props: {
|
||||
information: Property.MarkDown({
|
||||
value: 'Depending on the server you can also use this to move a file to another directory, as long as the directory exists.',
|
||||
variant: MarkdownVariant.INFO,
|
||||
}),
|
||||
oldPath: Property.ShortText({
|
||||
displayName: 'Old Path',
|
||||
required: true,
|
||||
description:
|
||||
'The path of the file or folder to rename e.g. `./myfolder/test.mp3`',
|
||||
}),
|
||||
newPath: Property.ShortText({
|
||||
displayName: 'New Path',
|
||||
required: true,
|
||||
description:
|
||||
'The new path of the file or folder e.g. `./myfolder/new-name.mp3`',
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const client = await getClient(context.auth.props);
|
||||
const oldPath = context.propsValue.oldPath;
|
||||
const newPath = context.propsValue.newPath;
|
||||
const protocolBackwardCompatibility = await getProtocolBackwardCompatibility(context.auth.props.protocol);
|
||||
try {
|
||||
switch (protocolBackwardCompatibility) {
|
||||
case 'ftps':
|
||||
case 'ftp':
|
||||
await renameFTP(client as FTPClient, oldPath, newPath);
|
||||
break;
|
||||
default:
|
||||
case 'sftp':
|
||||
await renameSFTP(client as Client, oldPath, newPath);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof FTPError) {
|
||||
console.error(getSftpError(err.code));
|
||||
return {
|
||||
status: 'error',
|
||||
content: null,
|
||||
error: getSftpError(err.code),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'error',
|
||||
content: null,
|
||||
error: err
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await endClient(client, context.auth.props.protocol);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import Client from 'ssh2-sftp-client';
|
||||
import { Client as FTPClient, FTPError } from 'basic-ftp';
|
||||
import { endClient, getClient, getProtocolBackwardCompatibility, sftpAuth } from '../..';
|
||||
import { Readable } from 'stream';
|
||||
import { getSftpError } from './common';
|
||||
|
||||
async function uploadFileToFTP(client: FTPClient, fileName: string, fileContent: { data: any }) {
|
||||
const remoteDirectory = fileName.substring(0, fileName.lastIndexOf('/'));
|
||||
await client.ensureDir(remoteDirectory);
|
||||
await client.uploadFrom(Readable.from(fileContent.data), fileName);
|
||||
}
|
||||
|
||||
async function uploadFileToSFTP(client: Client, fileName: string, fileContent: { data: any }) {
|
||||
const remotePathExists = await client.exists(fileName);
|
||||
if (!remotePathExists) {
|
||||
const remoteDirectory = fileName.substring(0, fileName.lastIndexOf('/'));
|
||||
await client.mkdir(remoteDirectory, true);
|
||||
}
|
||||
await client.put(fileContent.data, fileName);
|
||||
await client.end();
|
||||
}
|
||||
|
||||
export const uploadFileAction = createAction({
|
||||
auth: sftpAuth,
|
||||
name: 'upload_file',
|
||||
displayName: 'Upload File',
|
||||
description: 'Upload a file to the given path.',
|
||||
props: {
|
||||
fileName: Property.ShortText({
|
||||
displayName: 'File Path',
|
||||
required: true,
|
||||
description:
|
||||
'The path on the sftp server to store the file. e.g. `./myfolder/test.mp3`',
|
||||
}),
|
||||
fileContent: Property.File({
|
||||
displayName: 'File content',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const client = await getClient(context.auth.props);
|
||||
const fileName = context.propsValue['fileName'];
|
||||
const fileContent = context.propsValue['fileContent'];
|
||||
const protocolBackwardCompatibility = await getProtocolBackwardCompatibility(context.auth.props.protocol);
|
||||
try {
|
||||
switch (protocolBackwardCompatibility) {
|
||||
case 'ftps':
|
||||
case 'ftp':
|
||||
await uploadFileToFTP(client as FTPClient, fileName, fileContent);
|
||||
break;
|
||||
default:
|
||||
case 'sftp':
|
||||
await uploadFileToSFTP(client as Client, fileName, fileContent);
|
||||
break;
|
||||
}
|
||||
return {
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof FTPError) {
|
||||
console.error(getSftpError(err.code));
|
||||
return {
|
||||
status: 'error',
|
||||
content: null,
|
||||
error: getSftpError(err.code),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'error',
|
||||
content: null,
|
||||
error: err
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await endClient(client, context.auth.props.protocol);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { PiecePropValueSchema, Property, createTrigger, TriggerStrategy, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
|
||||
import { DedupeStrategy, Polling, pollingHelper } from '@activepieces/pieces-common';
|
||||
import { sftpAuth, getClient, getProtocolBackwardCompatibility, endClient } from '../..';
|
||||
import dayjs from 'dayjs';
|
||||
import Client from 'ssh2-sftp-client';
|
||||
import { Client as FTPClient, FileInfo as FTPFileInfo } from 'basic-ftp';
|
||||
|
||||
function getModifyTime(file: Client.FileInfo | FTPFileInfo, protocol: string): number {
|
||||
return protocol === 'sftp' ?
|
||||
(file as Client.FileInfo).modifyTime :
|
||||
dayjs((file as FTPFileInfo).modifiedAt).valueOf();
|
||||
}
|
||||
|
||||
const polling: Polling<AppConnectionValueForAuthProperty<typeof sftpAuth>, { path: string; ignoreHiddenFiles?: boolean }> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
|
||||
let client: Client | FTPClient | null = null;
|
||||
try {
|
||||
const protocolBackwardCompatibility = await getProtocolBackwardCompatibility(auth.props.protocol);
|
||||
client = await getClient(auth.props);
|
||||
const files = await client.list(propsValue.path);
|
||||
|
||||
const filteredFiles = files.filter(file => {
|
||||
const modTime = getModifyTime(file, protocolBackwardCompatibility);
|
||||
return dayjs(modTime).valueOf() > lastFetchEpochMS;
|
||||
});
|
||||
|
||||
const finalFiles: (Client.FileInfo | FTPFileInfo)[] = propsValue.ignoreHiddenFiles ?
|
||||
filteredFiles.filter(file => !file.name.startsWith('.')) :
|
||||
filteredFiles;
|
||||
|
||||
return finalFiles.map(file => {
|
||||
const modTime = getModifyTime(file, protocolBackwardCompatibility);
|
||||
|
||||
return {
|
||||
data: {
|
||||
...file,
|
||||
path: `${propsValue.path}/${file.name}`,
|
||||
},
|
||||
epochMilliSeconds: dayjs(modTime).valueOf(),
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
return [];
|
||||
} finally {
|
||||
if (client) {
|
||||
await endClient(client, auth.props.protocol);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const newOrModifiedFile = createTrigger({
|
||||
auth: sftpAuth,
|
||||
name: 'new_file',
|
||||
displayName: 'New File',
|
||||
description: 'Trigger when a new file is created or modified.',
|
||||
props: {
|
||||
path: Property.ShortText({
|
||||
displayName: 'Path',
|
||||
description: 'The path to watch for new files',
|
||||
required: true,
|
||||
defaultValue: './',
|
||||
}),
|
||||
ignoreHiddenFiles: Property.Checkbox({
|
||||
displayName: 'Ignore hidden files',
|
||||
description: 'Ignore hidden files',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
onEnable: async (context) => {
|
||||
await pollingHelper.onEnable(polling, context);
|
||||
},
|
||||
onDisable: async (context) => {
|
||||
await pollingHelper.onDisable(polling, context);
|
||||
},
|
||||
run: async (context) => {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
test: async (context) => {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
sampleData: null,
|
||||
});
|
||||
Reference in New Issue
Block a user