@akash-chowdhury-24/deployhub 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/install.ps1 +55 -0
  4. package/install.sh +99 -0
  5. package/package.json +86 -0
  6. package/src/adapters/dotnet.adapter.js +41 -0
  7. package/src/adapters/go.adapter.js +40 -0
  8. package/src/adapters/index.js +48 -0
  9. package/src/adapters/java.adapter.js +46 -0
  10. package/src/adapters/node.adapter.js +77 -0
  11. package/src/adapters/php.adapter.js +43 -0
  12. package/src/adapters/python.adapter.js +54 -0
  13. package/src/adapters/rails.adapter.js +66 -0
  14. package/src/artifact/engine.js +473 -0
  15. package/src/cli/index.js +45 -0
  16. package/src/commands/artifact.js +88 -0
  17. package/src/commands/build.js +44 -0
  18. package/src/commands/clean.js +50 -0
  19. package/src/commands/deploy.js +82 -0
  20. package/src/commands/doctor.js +630 -0
  21. package/src/commands/init.js +795 -0
  22. package/src/commands/logs.js +33 -0
  23. package/src/commands/rollback.js +50 -0
  24. package/src/commands/storage.js +116 -0
  25. package/src/commands/update.js +63 -0
  26. package/src/commands/verify.js +55 -0
  27. package/src/core/config.js +168 -0
  28. package/src/core/pipeline.js +77 -0
  29. package/src/core/stages.js +210 -0
  30. package/src/deployment/index.js +156 -0
  31. package/src/deployment/providers/azure-vm.js +7 -0
  32. package/src/deployment/providers/docker.js +30 -0
  33. package/src/deployment/providers/ec2.js +7 -0
  34. package/src/deployment/providers/gcp-vm.js +7 -0
  35. package/src/deployment/providers/kubernetes.js +30 -0
  36. package/src/deployment/providers/platforms/_shared.js +167 -0
  37. package/src/deployment/providers/platforms/aws-amplify.js +164 -0
  38. package/src/deployment/providers/platforms/azure-static-web-apps.js +68 -0
  39. package/src/deployment/providers/platforms/cloudflare-pages.js +103 -0
  40. package/src/deployment/providers/platforms/firebase-app-hosting.js +95 -0
  41. package/src/deployment/providers/platforms/firebase-hosting.js +99 -0
  42. package/src/deployment/providers/platforms/index.js +44 -0
  43. package/src/deployment/providers/platforms/netlify.js +102 -0
  44. package/src/deployment/providers/platforms/vercel.js +92 -0
  45. package/src/deployment/providers/ssh.js +365 -0
  46. package/src/detectors/angular.js +23 -0
  47. package/src/detectors/backend.detector.js +304 -0
  48. package/src/detectors/dotnet.js +18 -0
  49. package/src/detectors/frontend.detector.js +219 -0
  50. package/src/detectors/go.js +20 -0
  51. package/src/detectors/index.js +78 -0
  52. package/src/detectors/java.js +24 -0
  53. package/src/detectors/nextjs.js +23 -0
  54. package/src/detectors/node.js +28 -0
  55. package/src/detectors/php.js +17 -0
  56. package/src/detectors/python.js +22 -0
  57. package/src/detectors/react.js +29 -0
  58. package/src/detectors/vue.js +23 -0
  59. package/src/logger/index.js +44 -0
  60. package/src/notifications/email.js +53 -0
  61. package/src/notifications/index.js +34 -0
  62. package/src/notifications/slack.js +18 -0
  63. package/src/notifications/webhook.js +21 -0
  64. package/src/rollback/engine.js +102 -0
  65. package/src/storage/index.js +109 -0
  66. package/src/storage/providers/aws.js +96 -0
  67. package/src/storage/providers/azure.js +45 -0
  68. package/src/storage/providers/dropbox.js +49 -0
  69. package/src/storage/providers/ftp.js +69 -0
  70. package/src/storage/providers/gcp.js +45 -0
  71. package/src/storage/providers/gdrive.js +80 -0
  72. package/src/storage/providers/local.js +61 -0
  73. package/src/utils/author.js +141 -0
  74. package/src/utils/checksums.js +53 -0
  75. package/src/utils/firebase-config-generator.js +35 -0
  76. package/src/utils/github-actions.js +389 -0
  77. package/src/utils/init-platform.js +229 -0
  78. package/src/utils/nginx.js +34 -0
  79. package/src/utils/platform-env.js +132 -0
  80. package/src/utils/version.js +31 -0
@@ -0,0 +1,28 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ function detect(cwd = process.cwd()) {
5
+ return fs.existsSync(path.join(cwd, 'package.json'));
6
+ }
7
+
8
+ function getInfo(cwd = process.cwd()) {
9
+ const pkg = fs.readJsonSync(path.join(cwd, 'package.json'));
10
+ const scripts = pkg.scripts || {};
11
+ let buildCommand = 'npm install';
12
+ if (scripts.build) {
13
+ buildCommand = 'npm run build';
14
+ } else if (scripts.start) {
15
+ buildCommand = 'npm install';
16
+ }
17
+ const outputCandidates = ['dist', 'build', 'out', 'public'];
18
+ const buildOutput =
19
+ outputCandidates.find((d) => fs.existsSync(path.join(cwd, d))) || 'dist';
20
+ return {
21
+ framework: 'node',
22
+ buildCommand,
23
+ buildOutput,
24
+ hasDocker: fs.existsSync(path.join(cwd, 'Dockerfile')),
25
+ };
26
+ }
27
+
28
+ export default { detect, getInfo };
@@ -0,0 +1,17 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ function detect(cwd = process.cwd()) {
5
+ return fs.existsSync(path.join(cwd, 'composer.json'));
6
+ }
7
+
8
+ function getInfo(cwd = process.cwd()) {
9
+ return {
10
+ framework: 'php',
11
+ buildCommand: 'composer install --no-dev --optimize-autoloader',
12
+ buildOutput: 'public',
13
+ hasDocker: fs.existsSync(path.join(cwd, 'Dockerfile')),
14
+ };
15
+ }
16
+
17
+ export default { detect, getInfo };
@@ -0,0 +1,22 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ function detect(cwd = process.cwd()) {
5
+ return (
6
+ fs.existsSync(path.join(cwd, 'requirements.txt')) ||
7
+ fs.existsSync(path.join(cwd, 'pyproject.toml')) ||
8
+ fs.existsSync(path.join(cwd, 'setup.py'))
9
+ );
10
+ }
11
+
12
+ function getInfo(cwd = process.cwd()) {
13
+ const hasDocker = fs.existsSync(path.join(cwd, 'Dockerfile'));
14
+ return {
15
+ framework: 'python',
16
+ buildCommand: 'pip install -r requirements.txt',
17
+ buildOutput: 'dist',
18
+ hasDocker,
19
+ };
20
+ }
21
+
22
+ export default { detect, getInfo };
@@ -0,0 +1,29 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * @param {string} [cwd]
6
+ */
7
+ function detect(cwd = process.cwd()) {
8
+ const pkgPath = path.join(cwd, 'package.json');
9
+ if (!fs.existsSync(pkgPath)) return false;
10
+ const pkg = fs.readJsonSync(pkgPath);
11
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
12
+ return !!(deps.react && !deps.next);
13
+ }
14
+
15
+ /**
16
+ * @param {string} [cwd]
17
+ */
18
+ function getInfo(cwd = process.cwd()) {
19
+ const pkg = fs.readJsonSync(path.join(cwd, 'package.json'));
20
+ const scripts = pkg.scripts || {};
21
+ return {
22
+ framework: 'react',
23
+ buildCommand: scripts.build ? 'npm run build' : 'npm run build',
24
+ buildOutput: fs.existsSync(path.join(cwd, 'dist')) ? 'dist' : 'build',
25
+ hasDocker: fs.existsSync(path.join(cwd, 'Dockerfile')),
26
+ };
27
+ }
28
+
29
+ export default { detect, getInfo };
@@ -0,0 +1,23 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ function detect(cwd = process.cwd()) {
5
+ const pkgPath = path.join(cwd, 'package.json');
6
+ if (!fs.existsSync(pkgPath)) return false;
7
+ const pkg = fs.readJsonSync(pkgPath);
8
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
9
+ return !!deps.vue;
10
+ }
11
+
12
+ function getInfo(cwd = process.cwd()) {
13
+ const pkg = fs.readJsonSync(path.join(cwd, 'package.json'));
14
+ const scripts = pkg.scripts || {};
15
+ return {
16
+ framework: 'vue',
17
+ buildCommand: scripts.build ? 'npm run build' : 'npm run build',
18
+ buildOutput: fs.existsSync(path.join(cwd, 'dist')) ? 'dist' : 'build',
19
+ hasDocker: fs.existsSync(path.join(cwd, 'Dockerfile')),
20
+ };
21
+ }
22
+
23
+ export default { detect, getInfo };
@@ -0,0 +1,44 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * @param {string} stage
5
+ * @returns {string}
6
+ */
7
+ function timestamp() {
8
+ const now = new Date();
9
+ const h = String(now.getHours()).padStart(2, '0');
10
+ const m = String(now.getMinutes()).padStart(2, '0');
11
+ const s = String(now.getSeconds()).padStart(2, '0');
12
+ return `${h}:${m}:${s}`;
13
+ }
14
+
15
+ /**
16
+ * @param {string} stage
17
+ * @param {string} message
18
+ * @param {'info'|'success'|'warn'|'error'} level
19
+ */
20
+ function log(stage, message, level = 'info') {
21
+ const prefix = chalk.gray(`[${timestamp()}]`) + chalk.cyan(` [${stage}]`);
22
+ const colors = {
23
+ info: chalk.white,
24
+ success: chalk.green,
25
+ warn: chalk.yellow,
26
+ error: chalk.red,
27
+ };
28
+ console.log(`${prefix} ${colors[level](message)}`);
29
+ }
30
+
31
+ /**
32
+ * @param {string} stage
33
+ * @returns {{ info: Function, success: Function, warn: Function, error: Function }}
34
+ */
35
+ export function createLogger(stage) {
36
+ return {
37
+ info: (msg) => log(stage, msg, 'info'),
38
+ success: (msg) => log(stage, msg, 'success'),
39
+ warn: (msg) => log(stage, msg, 'warn'),
40
+ error: (msg) => log(stage, msg, 'error'),
41
+ };
42
+ }
43
+
44
+ export default { createLogger };
@@ -0,0 +1,53 @@
1
+ import nodemailer from 'nodemailer';
2
+ import { createLogger } from '../logger/index.js';
3
+
4
+ /**
5
+ * @param {import('../core/config.js').DeployHubConfig} config
6
+ * @param {{ success: boolean, version: string, message?: string, deployUrl?: string, environment?: string }} result
7
+ */
8
+ export async function sendEmailNotification(config, result) {
9
+ const log = createLogger('email');
10
+
11
+ const host = process.env.SMTP_HOST;
12
+ const port = parseInt(process.env.SMTP_PORT || '587', 10);
13
+ const user = process.env.SMTP_USER;
14
+ const pass = process.env.SMTP_PASS;
15
+ const to = process.env.NOTIFY_EMAIL_TO || process.env.NOTIFICATION_EMAIL;
16
+
17
+ if (!host || !to) {
18
+ throw new Error('SMTP_HOST and NOTIFY_EMAIL_TO (or NOTIFICATION_EMAIL) must be set');
19
+ }
20
+
21
+ const transporter = nodemailer.createTransport({
22
+ host,
23
+ port,
24
+ secure: port === 465,
25
+ auth: user && pass ? { user, pass } : undefined,
26
+ });
27
+
28
+ const status = result.success ? 'SUCCESS' : 'FAILED';
29
+ const environment = result.environment || process.env.DEPLOYHUB_ENV || 'production';
30
+ const deployUrl = result.deployUrl || config.healthCheck?.url || 'N/A';
31
+
32
+ const subject = `[DeployHub] ${status}: ${config.project} v${result.version} (${environment})`;
33
+ const body = [
34
+ `Project: ${config.project}`,
35
+ `Version: ${result.version}`,
36
+ `Environment: ${environment}`,
37
+ `Status: ${status}`,
38
+ `Deploy URL: ${deployUrl}`,
39
+ '',
40
+ result.message || (result.success ? 'Deployment completed successfully.' : 'Deployment failed.'),
41
+ ].join('\n');
42
+
43
+ await transporter.sendMail({
44
+ from: user || `deployhub@${host}`,
45
+ to,
46
+ subject,
47
+ text: body,
48
+ });
49
+
50
+ log.success(`Email notification sent to ${to}`);
51
+ }
52
+
53
+ export default { sendEmailNotification };
@@ -0,0 +1,34 @@
1
+ import axios from 'axios';
2
+ import { createLogger } from '../logger/index.js';
3
+ import { sendSlackNotification } from './slack.js';
4
+ import { sendEmailNotification } from './email.js';
5
+ import { sendWebhookNotification } from './webhook.js';
6
+
7
+ /**
8
+ * @param {import('../core/config.js').DeployHubConfig} config
9
+ * @param {{ success: boolean, version: string, message?: string, deployUrl?: string, environment?: string }} result
10
+ */
11
+ export async function sendNotifications(config, result) {
12
+ const log = createLogger('notify');
13
+ const tasks = [];
14
+
15
+ if (config.notifications.slack) {
16
+ tasks.push(sendSlackNotification(result));
17
+ }
18
+ if (config.notifications.email) {
19
+ tasks.push(sendEmailNotification(config, result));
20
+ }
21
+ if (config.notifications.webhook) {
22
+ tasks.push(sendWebhookNotification(result));
23
+ }
24
+
25
+ if (tasks.length === 0) {
26
+ log.info('No notifications configured');
27
+ return;
28
+ }
29
+
30
+ await Promise.allSettled(tasks);
31
+ log.success('Notifications sent');
32
+ }
33
+
34
+ export default { sendNotifications };
@@ -0,0 +1,18 @@
1
+ import axios from 'axios';
2
+
3
+ /**
4
+ * @param {{ success: boolean, version: string, message?: string }} result
5
+ */
6
+ export async function sendSlackNotification(result) {
7
+ const webhookUrl = process.env.SLACK_WEBHOOK_URL;
8
+ if (!webhookUrl) {
9
+ throw new Error('SLACK_WEBHOOK_URL not set');
10
+ }
11
+
12
+ const status = result.success ? '✅' : '❌';
13
+ await axios.post(webhookUrl, {
14
+ text: `${status} DeployHub: v${result.version} — ${result.message || (result.success ? 'Deployment succeeded' : 'Deployment failed')}`,
15
+ });
16
+ }
17
+
18
+ export default { sendSlackNotification };
@@ -0,0 +1,21 @@
1
+ import axios from 'axios';
2
+
3
+ /**
4
+ * @param {{ success: boolean, version: string, message?: string }} result
5
+ */
6
+ export async function sendWebhookNotification(result) {
7
+ const url = process.env.WEBHOOK_URL;
8
+ if (!url) {
9
+ throw new Error('WEBHOOK_URL not set');
10
+ }
11
+
12
+ await axios.post(url, {
13
+ event: 'deployhub.deployment',
14
+ success: result.success,
15
+ version: result.version,
16
+ message: result.message,
17
+ timestamp: new Date().toISOString(),
18
+ });
19
+ }
20
+
21
+ export default { sendWebhookNotification };
@@ -0,0 +1,102 @@
1
+ import { downloadFromFirst } from '../storage/index.js';
2
+ import { createPlatformProvider } from '../deployment/providers/platforms/index.js';
3
+ import { extractArtifact } from '../artifact/engine.js';
4
+ import { createLogger } from '../logger/index.js';
5
+ import fs from 'fs-extra';
6
+ import path from 'path';
7
+
8
+ /**
9
+ * @param {import('../core/config.js').DeployHubConfig} config
10
+ * @param {string} artifactDir
11
+ * @param {string} envName
12
+ */
13
+ async function rollbackPlatformTarget(config, artifactDir, envName) {
14
+ const envConfig = config.environments[envName];
15
+ if (!envConfig) {
16
+ throw new Error(`Environment "${envName}" not found in config`);
17
+ }
18
+
19
+ const deploymentPath = path.join(artifactDir, 'deployment.json');
20
+ if (await fs.pathExists(deploymentPath)) {
21
+ const data = await fs.readJson(deploymentPath);
22
+ const deployments = data.deployments || data.platformDeployments || [];
23
+ const record = deployments.find((d) => d.environmentName === envName) || data.lastDeployment;
24
+ if (record?.platform) {
25
+ const log = createLogger('rollback');
26
+ log.info(
27
+ `Rolling back ${envName} on ${record.platform} (deploy ${record.deployId || record.deploymentId || 'previous'})...`
28
+ );
29
+ }
30
+ }
31
+
32
+ if (envConfig.deploymentType === 'platform' || envConfig.frontendDeploymentType === 'platform') {
33
+ const platform = envConfig.platform;
34
+ if (!platform) {
35
+ throw new Error(`No platform configured for environment "${envName}"`);
36
+ }
37
+ const provider = createPlatformProvider(platform, config, envName);
38
+ await provider.rollback(artifactDir);
39
+ return;
40
+ }
41
+
42
+ if (
43
+ envConfig.frontendDeploymentType === 'platform' &&
44
+ (envConfig.backendDeploymentType === 'server' || envConfig.type)
45
+ ) {
46
+ const platformProvider = createPlatformProvider(envConfig.platform, config, envName);
47
+ await platformProvider.rollback(artifactDir);
48
+ const { getDeploymentProvider } = await import('../deployment/index.js');
49
+ const serverProvider = getDeploymentProvider(envConfig.type, config, envName);
50
+ await serverProvider.rollback(artifactDir);
51
+ return;
52
+ }
53
+
54
+ const { getDeploymentProvider } = await import('../deployment/index.js');
55
+ const provider = getDeploymentProvider(envConfig.type, config, envName);
56
+ await provider.rollback(artifactDir);
57
+ }
58
+
59
+ /**
60
+ * @param {import('../core/config.js').DeployHubConfig} config
61
+ * @param {string} version
62
+ * @param {string} [cwd]
63
+ */
64
+ export async function rollbackToVersion(config, version, cwd = process.cwd()) {
65
+ const log = createLogger('rollback');
66
+ const remoteKey = `${config.project}/v${version}/artifact.zip`;
67
+ const restoreDir = path.join(cwd, '.deployhub-restore', `v${version}`);
68
+ const artifactDir = path.join(restoreDir, 'artifact');
69
+
70
+ log.info(`Downloading artifact v${version}...`);
71
+ await fs.emptyDir(restoreDir);
72
+ await fs.ensureDir(artifactDir);
73
+
74
+ const zipPath = path.join(artifactDir, 'artifact.zip');
75
+ await downloadFromFirst(config.storage, remoteKey, zipPath);
76
+
77
+ log.info('Extracting artifact for rollback...');
78
+ const extractedDir = path.join(artifactDir, '_extracted');
79
+ await fs.emptyDir(extractedDir);
80
+ await extractArtifact(artifactDir, extractedDir);
81
+
82
+ const extractedDeployment = path.join(extractedDir, 'deployment.json');
83
+ if (await fs.pathExists(extractedDeployment)) {
84
+ await fs.copy(extractedDeployment, path.join(artifactDir, 'deployment.json'));
85
+ }
86
+
87
+ const targets = config.deploy || [];
88
+ if (targets.length === 0) {
89
+ log.warn('No deployment targets configured');
90
+ return artifactDir;
91
+ }
92
+
93
+ log.info('Executing platform-specific rollback from deployment.json...');
94
+ for (const envName of targets) {
95
+ await rollbackPlatformTarget(config, artifactDir, envName);
96
+ }
97
+
98
+ log.success(`Rollback to v${version} complete`);
99
+ return artifactDir;
100
+ }
101
+
102
+ export default { rollbackToVersion };
@@ -0,0 +1,109 @@
1
+ import path from 'path';
2
+ import { createLogger } from '../logger/index.js';
3
+ import { createAwsProvider } from './providers/aws.js';
4
+ import { createLocalProvider } from './providers/local.js';
5
+ import { createAzureProvider } from './providers/azure.js';
6
+ import { createGcpProvider } from './providers/gcp.js';
7
+ import { createGdriveProvider } from './providers/gdrive.js';
8
+ import { createDropboxProvider } from './providers/dropbox.js';
9
+ import { createFtpProvider } from './providers/ftp.js';
10
+
11
+ /** @type {Record<string, (env?: Record<string, string>) => import('./providers/aws.js').default>} */
12
+ const PROVIDER_FACTORIES = {
13
+ aws: createAwsProvider,
14
+ local: createLocalProvider,
15
+ azure: createAzureProvider,
16
+ gcp: createGcpProvider,
17
+ gdrive: createGdriveProvider,
18
+ dropbox: createDropboxProvider,
19
+ ftp: createFtpProvider,
20
+ };
21
+
22
+ /**
23
+ * @param {string} name
24
+ * @param {Record<string, string>} [env]
25
+ */
26
+ export function getStorageProvider(name, env = process.env) {
27
+ const factory = PROVIDER_FACTORIES[name];
28
+ if (!factory) {
29
+ throw new Error(`Unknown storage provider: ${name}`);
30
+ }
31
+ return factory(env);
32
+ }
33
+
34
+ /**
35
+ * @param {string[]} providers
36
+ * @param {string} zipPath
37
+ * @param {import('../core/config.js').DeployHubConfig} config
38
+ */
39
+ export async function uploadToAll(providers, zipPath, config) {
40
+ const log = createLogger('storage');
41
+ const remoteKey = `${config.project}/v${config.version}/artifact.zip`;
42
+
43
+ const uploads = providers.map(async (name) => {
44
+ try {
45
+ const provider = getStorageProvider(name);
46
+ log.info(`Uploading to ${name}...`);
47
+ await provider.upload(zipPath, remoteKey);
48
+ log.success(`Uploaded to ${name}`);
49
+ return { name, success: true };
50
+ } catch (err) {
51
+ const message = err instanceof Error ? err.message : String(err);
52
+ throw new Error(`Storage upload to ${name} failed: ${message}`);
53
+ }
54
+ });
55
+
56
+ return Promise.all(uploads);
57
+ }
58
+
59
+ /**
60
+ * @param {string[]} providers
61
+ * @param {string} remoteKey
62
+ * @param {string} localPath
63
+ */
64
+ export async function downloadFromFirst(providers, remoteKey, localPath) {
65
+ for (const name of providers) {
66
+ const provider = getStorageProvider(name);
67
+ const exists = await provider.verify(remoteKey);
68
+ if (exists) {
69
+ await provider.download(remoteKey, localPath);
70
+ return name;
71
+ }
72
+ }
73
+ throw new Error(`Artifact not found in any configured storage provider`);
74
+ }
75
+
76
+ /**
77
+ * @param {string} name
78
+ */
79
+ export async function testProvider(name) {
80
+ const provider = getStorageProvider(name);
81
+ await provider.testConnection();
82
+ }
83
+
84
+ /**
85
+ * @param {string[]} providers
86
+ */
87
+ export async function testAllProviders(providers) {
88
+ const results = await Promise.allSettled(
89
+ providers.map(async (name) => {
90
+ await testProvider(name);
91
+ return { name, status: 'connected' };
92
+ })
93
+ );
94
+
95
+ return results.map((result, i) => {
96
+ const name = providers[i];
97
+ if (result.status === 'fulfilled') {
98
+ return { name, status: 'connected' };
99
+ }
100
+ return {
101
+ name,
102
+ status: 'error',
103
+ error: result.reason?.message || String(result.reason),
104
+ };
105
+ });
106
+ }
107
+
108
+ export { PROVIDER_FACTORIES };
109
+ export default { getStorageProvider, uploadToAll, downloadFromFirst, testProvider, testAllProviders };
@@ -0,0 +1,96 @@
1
+ import { S3Client, HeadBucketCommand, DeleteObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
2
+ import { Upload } from '@aws-sdk/lib-storage';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+
6
+ /**
7
+ * @param {Record<string, string>} env
8
+ */
9
+ export function createAwsProvider(env = process.env) {
10
+ const bucket = env.AWS_BUCKET;
11
+ const region = env.AWS_REGION || 'us-east-1';
12
+
13
+ if (!env.AWS_ACCESS_KEY_ID || !env.AWS_SECRET_ACCESS_KEY || !bucket) {
14
+ throw new Error(
15
+ 'AWS credentials incomplete. Set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET in .env'
16
+ );
17
+ }
18
+
19
+ const client = new S3Client({
20
+ region,
21
+ credentials: {
22
+ accessKeyId: env.AWS_ACCESS_KEY_ID,
23
+ secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
24
+ },
25
+ });
26
+
27
+ /**
28
+ * @param {string} localPath
29
+ * @param {string} [remoteKey]
30
+ */
31
+ async function upload(localPath, remoteKey) {
32
+ const key =
33
+ remoteKey ||
34
+ path.basename(localPath).replace(/\\/g, '/');
35
+
36
+ const fileStream = fs.createReadStream(localPath);
37
+ const upload = new Upload({
38
+ client,
39
+ params: {
40
+ Bucket: bucket,
41
+ Key: key,
42
+ Body: fileStream,
43
+ },
44
+ });
45
+ await upload.done();
46
+ }
47
+
48
+ /**
49
+ * @param {string} remoteKey
50
+ * @param {string} localPath
51
+ */
52
+ async function download(remoteKey, localPath) {
53
+ const response = await client.send(
54
+ new GetObjectCommand({ Bucket: bucket, Key: remoteKey })
55
+ );
56
+ await fs.ensureDir(path.dirname(localPath));
57
+ const body = response.Body;
58
+ if (!body) throw new Error('Empty response from S3');
59
+ const chunks = [];
60
+ for await (const chunk of body) {
61
+ chunks.push(chunk);
62
+ }
63
+ await fs.writeFile(localPath, Buffer.concat(chunks));
64
+ }
65
+
66
+ /**
67
+ * @param {string} remoteKey
68
+ */
69
+ async function verify(remoteKey) {
70
+ try {
71
+ await client.send(
72
+ new GetObjectCommand({ Bucket: bucket, Key: remoteKey })
73
+ );
74
+ return true;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * @param {string} remoteKey
82
+ */
83
+ async function deleteObject(remoteKey) {
84
+ await client.send(
85
+ new DeleteObjectCommand({ Bucket: bucket, Key: remoteKey })
86
+ );
87
+ }
88
+
89
+ async function testConnection() {
90
+ await client.send(new HeadBucketCommand({ Bucket: bucket }));
91
+ }
92
+
93
+ return { upload, download, verify, delete: deleteObject, testConnection };
94
+ }
95
+
96
+ export default { createAwsProvider };
@@ -0,0 +1,45 @@
1
+ import { BlobServiceClient } from '@azure/storage-blob';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+
5
+ export function createAzureProvider(env = process.env) {
6
+ const connectionString = env.AZURE_CONNECTION_STRING;
7
+ const container = env.AZURE_CONTAINER;
8
+
9
+ if (!connectionString || !container) {
10
+ throw new Error('Azure credentials incomplete. Set AZURE_CONNECTION_STRING and AZURE_CONTAINER.');
11
+ }
12
+
13
+ const client = BlobServiceClient.fromConnectionString(connectionString);
14
+ const containerClient = client.getContainerClient(container);
15
+
16
+ async function upload(localPath, remoteKey) {
17
+ const key = remoteKey || path.basename(localPath);
18
+ const blockBlob = containerClient.getBlockBlobClient(key);
19
+ await blockBlob.uploadFile(localPath);
20
+ }
21
+
22
+ async function download(remoteKey, localPath) {
23
+ const blockBlob = containerClient.getBlockBlobClient(remoteKey);
24
+ await fs.ensureDir(path.dirname(localPath));
25
+ await blockBlob.downloadToFile(localPath);
26
+ }
27
+
28
+ async function verify(remoteKey) {
29
+ const blockBlob = containerClient.getBlockBlobClient(remoteKey);
30
+ return blockBlob.exists();
31
+ }
32
+
33
+ async function deleteObject(remoteKey) {
34
+ const blockBlob = containerClient.getBlockBlobClient(remoteKey);
35
+ await blockBlob.deleteIfExists();
36
+ }
37
+
38
+ async function testConnection() {
39
+ await containerClient.getProperties();
40
+ }
41
+
42
+ return { upload, download, verify, delete: deleteObject, testConnection };
43
+ }
44
+
45
+ export default { createAzureProvider };