@things-factory/integration-sftp 4.3.660 → 4.3.668
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.
- package/CHANGELOG.md +369 -0
- package/dist-server/controllers/herbalife/herbalife.js +9 -5
- package/dist-server/controllers/herbalife/herbalife.js.map +1 -1
- package/dist-server/controllers/index.js +1 -0
- package/dist-server/controllers/index.js.map +1 -1
- package/dist-server/controllers/sftp-api/index.js +7 -0
- package/dist-server/controllers/sftp-api/index.js.map +1 -1
- package/dist-server/controllers/yltc/apis/create-yltc-inventory-report.js +40 -0
- package/dist-server/controllers/yltc/apis/create-yltc-inventory-report.js.map +1 -0
- package/dist-server/controllers/yltc/apis/echo.js +19 -0
- package/dist-server/controllers/yltc/apis/echo.js.map +1 -0
- package/dist-server/controllers/yltc/apis/index.js +19 -0
- package/dist-server/controllers/yltc/apis/index.js.map +1 -0
- package/dist-server/controllers/yltc/index.js +34 -0
- package/dist-server/controllers/yltc/index.js.map +1 -0
- package/dist-server/controllers/yltc/platform-action.js +30 -0
- package/dist-server/controllers/yltc/platform-action.js.map +1 -0
- package/dist-server/controllers/yltc/yltc.js +71 -0
- package/dist-server/controllers/yltc/yltc.js.map +1 -0
- package/dist-server/service/sftp/sftp-mutation.js +15 -8
- package/dist-server/service/sftp/sftp-mutation.js.map +1 -1
- package/dist-server/service/sftp/sftp.js +41 -0
- package/dist-server/service/sftp/sftp.js.map +1 -1
- package/dist-server/sftp-const.js +36 -2
- package/dist-server/sftp-const.js.map +1 -1
- package/dist-server/storage/providers/ftp-storage.provider.js +178 -0
- package/dist-server/storage/providers/ftp-storage.provider.js.map +1 -0
- package/dist-server/storage/providers/local-storage.provider.js +153 -0
- package/dist-server/storage/providers/local-storage.provider.js.map +1 -0
- package/dist-server/storage/providers/s3-storage.provider.js +181 -0
- package/dist-server/storage/providers/s3-storage.provider.js.map +1 -0
- package/dist-server/storage/providers/sftp-storage.provider.js +133 -0
- package/dist-server/storage/providers/sftp-storage.provider.js.map +1 -0
- package/dist-server/storage/storage-factory.js +55 -0
- package/dist-server/storage/storage-factory.js.map +1 -0
- package/dist-server/storage/storage-manager.js +86 -0
- package/dist-server/storage/storage-manager.js.map +1 -0
- package/dist-server/storage/storage-provider.interface.js +3 -0
- package/dist-server/storage/storage-provider.interface.js.map +1 -0
- package/dist-server/util/file-formatters.js +100 -0
- package/dist-server/util/file-formatters.js.map +1 -0
- package/dist-server/util/generate-files.js +77 -17
- package/dist-server/util/generate-files.js.map +1 -1
- package/dist-server/util/get-permitted-directories.js +9 -5
- package/dist-server/util/get-permitted-directories.js.map +1 -1
- package/package.json +6 -3
- package/server/controllers/herbalife/herbalife.ts +11 -6
- package/server/controllers/index.ts +1 -0
- package/server/controllers/sftp-api/index.ts +3 -0
- package/server/controllers/yltc/apis/create-yltc-inventory-report.ts +37 -0
- package/server/controllers/yltc/apis/echo.ts +14 -0
- package/server/controllers/yltc/apis/index.ts +2 -0
- package/server/controllers/yltc/index.ts +7 -0
- package/server/controllers/yltc/platform-action.ts +34 -0
- package/server/controllers/yltc/yltc.ts +93 -0
- package/server/service/sftp/sftp-mutation.ts +16 -10
- package/server/service/sftp/sftp.ts +34 -0
- package/server/sftp-const.ts +41 -1
- package/server/storage/providers/ftp-storage.provider.ts +177 -0
- package/server/storage/providers/local-storage.provider.ts +159 -0
- package/server/storage/providers/s3-storage.provider.ts +214 -0
- package/server/storage/providers/sftp-storage.provider.ts +157 -0
- package/server/storage/storage-factory.ts +62 -0
- package/server/storage/storage-manager.ts +103 -0
- package/server/storage/storage-provider.interface.ts +42 -0
- package/server/util/file-formatters.ts +97 -0
- package/server/util/generate-files.ts +79 -17
- package/server/util/get-permitted-directories.ts +11 -7
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import Client from 'ssh2-sftp-client'
|
|
2
|
+
import { StorageProvider, StorageConfig } from '../storage-provider.interface'
|
|
3
|
+
import { logger } from '@things-factory/env'
|
|
4
|
+
|
|
5
|
+
export class SftpStorageProvider implements StorageProvider {
|
|
6
|
+
private sftp: Client
|
|
7
|
+
private config: StorageConfig
|
|
8
|
+
private connected: boolean = false
|
|
9
|
+
|
|
10
|
+
constructor(config: StorageConfig) {
|
|
11
|
+
this.config = config
|
|
12
|
+
this.sftp = new Client()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async connect(): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
const connectionConfig = {
|
|
18
|
+
host: this.config.host!,
|
|
19
|
+
port: this.config.port || 22,
|
|
20
|
+
username: this.config.username!,
|
|
21
|
+
password: this.config.password,
|
|
22
|
+
privateKey: this.config.privateKey,
|
|
23
|
+
readyTimeout: this.config.timeout || 20000,
|
|
24
|
+
retries: this.config.retries || 3
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await this.sftp.connect(connectionConfig)
|
|
28
|
+
this.connected = true
|
|
29
|
+
logger.info('SFTP Storage Provider connected successfully')
|
|
30
|
+
} catch (error) {
|
|
31
|
+
this.connected = false
|
|
32
|
+
throw new Error(`Failed to connect to SFTP: ${error}`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async disconnect(): Promise<void> {
|
|
37
|
+
if (this.connected) {
|
|
38
|
+
await this.sftp.end()
|
|
39
|
+
this.connected = false
|
|
40
|
+
logger.info('SFTP Storage Provider disconnected')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
isConnected(): boolean {
|
|
45
|
+
return this.connected
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async readFile(path: string, encoding?: string): Promise<any> {
|
|
49
|
+
const fullPath = this.getFullPath(path)
|
|
50
|
+
const buffer = await this.sftp.get(fullPath)
|
|
51
|
+
|
|
52
|
+
if (encoding) {
|
|
53
|
+
return buffer.toString(encoding)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return buffer
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async writeFile(path: string, content: any, encoding?: string): Promise<void> {
|
|
60
|
+
const fullPath = this.getFullPath(path)
|
|
61
|
+
const buffer = Buffer.from(content)
|
|
62
|
+
|
|
63
|
+
await this.sftp.put(buffer, fullPath)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async uploadFile(params: {
|
|
67
|
+
stream: any
|
|
68
|
+
filename: string
|
|
69
|
+
uploadPath: string
|
|
70
|
+
}): Promise<{ id: string; path: string; size: number }> {
|
|
71
|
+
const fullPath = this.getFullPath(`${params.uploadPath}/${params.filename}`)
|
|
72
|
+
let size = 0
|
|
73
|
+
|
|
74
|
+
return new Promise<{ id: string; path: string; size: number }>((resolve, reject) => {
|
|
75
|
+
const uploadStream = this.sftp.createWriteStream(fullPath)
|
|
76
|
+
|
|
77
|
+
// Safety timeout to avoid hanging indefinitely
|
|
78
|
+
const timeoutMs = (this.config.timeout as number) || 30000
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
reject(new Error(`SFTP upload timed out for ${fullPath}`))
|
|
81
|
+
}, timeoutMs)
|
|
82
|
+
|
|
83
|
+
const onResolve = () => {
|
|
84
|
+
clearTimeout(timer)
|
|
85
|
+
resolve({ id: Date.now().toString(), path: params.uploadPath, size })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
uploadStream.on('error', (error: any) => {
|
|
89
|
+
clearTimeout(timer)
|
|
90
|
+
reject(error)
|
|
91
|
+
})
|
|
92
|
+
uploadStream.on('close', onResolve)
|
|
93
|
+
uploadStream.on('finish', onResolve)
|
|
94
|
+
|
|
95
|
+
params.stream
|
|
96
|
+
.on('error', (error: any) => {
|
|
97
|
+
clearTimeout(timer)
|
|
98
|
+
reject(error)
|
|
99
|
+
})
|
|
100
|
+
.on('data', (chunk: any) => (size += chunk.length))
|
|
101
|
+
.pipe(uploadStream)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async deleteFile(path: string): Promise<void> {
|
|
106
|
+
const fullPath = this.getFullPath(path)
|
|
107
|
+
await this.sftp.delete(fullPath)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async moveFile(params: { source: string; destination: string }): Promise<void> {
|
|
111
|
+
const sourcePath = this.getFullPath(params.source)
|
|
112
|
+
const destPath = this.getFullPath(params.destination)
|
|
113
|
+
|
|
114
|
+
await this.sftp.rename(sourcePath, destPath)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async readFolders(params: { path: string }): Promise<any[]> {
|
|
118
|
+
const fullPath = this.getFullPath(params.path)
|
|
119
|
+
const list = await this.sftp.list(fullPath)
|
|
120
|
+
|
|
121
|
+
return list.filter(item => item.type === '-') // Only files, not directories
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async createDirectory(path: string): Promise<void> {
|
|
125
|
+
const fullPath = this.getFullPath(path)
|
|
126
|
+
await this.sftp.mkdir(fullPath, true) // true for recursive
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async deleteDirectory(path: string): Promise<void> {
|
|
130
|
+
const fullPath = this.getFullPath(path)
|
|
131
|
+
await this.sftp.rmdir(fullPath, true) // true for recursive
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async fileExists(path: string): Promise<boolean> {
|
|
135
|
+
try {
|
|
136
|
+
const fullPath = this.getFullPath(path)
|
|
137
|
+
await this.sftp.stat(fullPath)
|
|
138
|
+
return true
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async getFileSize(path: string): Promise<number> {
|
|
145
|
+
const fullPath = this.getFullPath(path)
|
|
146
|
+
const stats = await this.sftp.stat(fullPath)
|
|
147
|
+
return stats.size
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private getFullPath(path: string): string {
|
|
151
|
+
if (this.config.basePath) {
|
|
152
|
+
const base = this.config.basePath.startsWith('/') ? this.config.basePath : `/${this.config.basePath}`
|
|
153
|
+
return `${base}/${path}`.replace(/\/+/g, '/')
|
|
154
|
+
}
|
|
155
|
+
return path.startsWith('/') ? path : `/${path}`
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { StorageProvider, StorageConfig } from './storage-provider.interface'
|
|
2
|
+
import { S3StorageProvider } from './providers/s3-storage.provider'
|
|
3
|
+
import { SftpStorageProvider } from './providers/sftp-storage.provider'
|
|
4
|
+
import { FtpStorageProvider } from './providers/ftp-storage.provider'
|
|
5
|
+
import { LocalStorageProvider } from './providers/local-storage.provider'
|
|
6
|
+
|
|
7
|
+
export class StorageFactory {
|
|
8
|
+
private static providers: Map<string, StorageProvider> = new Map()
|
|
9
|
+
|
|
10
|
+
static createProvider(config: StorageConfig): StorageProvider {
|
|
11
|
+
const key = this.generateConfigKey(config)
|
|
12
|
+
|
|
13
|
+
if (this.providers.has(key)) {
|
|
14
|
+
return this.providers.get(key)!
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let provider: StorageProvider
|
|
18
|
+
|
|
19
|
+
switch (config.type) {
|
|
20
|
+
case 's3':
|
|
21
|
+
provider = new S3StorageProvider(config)
|
|
22
|
+
break
|
|
23
|
+
case 'sftp':
|
|
24
|
+
provider = new SftpStorageProvider(config)
|
|
25
|
+
break
|
|
26
|
+
case 'ftp':
|
|
27
|
+
provider = new FtpStorageProvider(config)
|
|
28
|
+
break
|
|
29
|
+
case 'local':
|
|
30
|
+
provider = new LocalStorageProvider(config)
|
|
31
|
+
break
|
|
32
|
+
default:
|
|
33
|
+
throw new Error(`Unsupported storage type: ${config.type}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.providers.set(key, provider)
|
|
37
|
+
return provider
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static async getProvider(config: StorageConfig): Promise<StorageProvider> {
|
|
41
|
+
const provider = this.createProvider(config)
|
|
42
|
+
|
|
43
|
+
if (!provider.isConnected()) {
|
|
44
|
+
await provider.connect()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return provider
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static async disconnectAll(): Promise<void> {
|
|
51
|
+
for (const provider of this.providers.values()) {
|
|
52
|
+
if (provider.isConnected()) {
|
|
53
|
+
await provider.disconnect()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.providers.clear()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static generateConfigKey(config: StorageConfig): string {
|
|
60
|
+
return `${config.type}-${config.host || config.bucketName || 'local'}-${config.username || 'default'}`
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { StorageFactory } from './storage-factory'
|
|
2
|
+
import { StorageProvider, StorageConfig } from './storage-provider.interface'
|
|
3
|
+
import { logger } from '@things-factory/env'
|
|
4
|
+
|
|
5
|
+
export class StorageManager {
|
|
6
|
+
private static instance: StorageManager
|
|
7
|
+
private providers: Map<string, StorageProvider> = new Map()
|
|
8
|
+
|
|
9
|
+
private constructor() {}
|
|
10
|
+
|
|
11
|
+
static getInstance(): StorageManager {
|
|
12
|
+
if (!StorageManager.instance) {
|
|
13
|
+
StorageManager.instance = new StorageManager()
|
|
14
|
+
}
|
|
15
|
+
return StorageManager.instance
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async getProvider(config: StorageConfig): Promise<StorageProvider> {
|
|
19
|
+
const key = this.generateConfigKey(config)
|
|
20
|
+
|
|
21
|
+
if (!this.providers.has(key)) {
|
|
22
|
+
const provider = StorageFactory.createProvider(config)
|
|
23
|
+
await provider.connect()
|
|
24
|
+
this.providers.set(key, provider)
|
|
25
|
+
logger.info(`Storage provider created for: ${key}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const provider = this.providers.get(key)!
|
|
29
|
+
// Reconnect if the cached provider has been disconnected (e.g., idle timeout)
|
|
30
|
+
if (!provider.isConnected()) {
|
|
31
|
+
logger.info(`Storage provider re-connecting: ${key}`)
|
|
32
|
+
await provider.connect()
|
|
33
|
+
}
|
|
34
|
+
return provider
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async readFile(config: StorageConfig, path: string, encoding?: string): Promise<any> {
|
|
38
|
+
const provider = await this.getProvider(config)
|
|
39
|
+
return await provider.readFile(path, encoding)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async writeFile(config: StorageConfig, path: string, content: any, encoding?: string): Promise<void> {
|
|
43
|
+
const provider = await this.getProvider(config)
|
|
44
|
+
await provider.writeFile(path, content, encoding)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async uploadFile(
|
|
48
|
+
config: StorageConfig,
|
|
49
|
+
params: { stream: any; filename: string; uploadPath: string }
|
|
50
|
+
): Promise<{ id: string; path: string; size: number }> {
|
|
51
|
+
const provider = await this.getProvider(config)
|
|
52
|
+
return await provider.uploadFile(params)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async deleteFile(config: StorageConfig, path: string): Promise<void> {
|
|
56
|
+
const provider = await this.getProvider(config)
|
|
57
|
+
await provider.deleteFile(path)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async moveFile(config: StorageConfig, params: { source: string; destination: string }): Promise<void> {
|
|
61
|
+
const provider = await this.getProvider(config)
|
|
62
|
+
await provider.moveFile(params)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async readFolders(config: StorageConfig, params: { path: string }): Promise<any[]> {
|
|
66
|
+
const provider = await this.getProvider(config)
|
|
67
|
+
return await provider.readFolders(params)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createDirectory(config: StorageConfig, path: string): Promise<void> {
|
|
71
|
+
const provider = await this.getProvider(config)
|
|
72
|
+
await provider.createDirectory(path)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async deleteDirectory(config: StorageConfig, path: string): Promise<void> {
|
|
76
|
+
const provider = await this.getProvider(config)
|
|
77
|
+
await provider.deleteDirectory(path)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async fileExists(config: StorageConfig, path: string): Promise<boolean> {
|
|
81
|
+
const provider = await this.getProvider(config)
|
|
82
|
+
return await provider.fileExists(path)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async getFileSize(config: StorageConfig, path: string): Promise<number> {
|
|
86
|
+
const provider = await this.getProvider(config)
|
|
87
|
+
return await provider.getFileSize(path)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async disconnectAll(): Promise<void> {
|
|
91
|
+
for (const [key, provider] of this.providers.entries()) {
|
|
92
|
+
if (provider.isConnected()) {
|
|
93
|
+
await provider.disconnect()
|
|
94
|
+
logger.info(`Storage provider disconnected: ${key}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
this.providers.clear()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private generateConfigKey(config: StorageConfig): string {
|
|
101
|
+
return `${config.type}-${config.host || config.bucketName || 'local'}-${config.username || 'default'}`
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface StorageProvider {
|
|
2
|
+
// File operations
|
|
3
|
+
readFile(path: string, encoding?: string): Promise<any>
|
|
4
|
+
writeFile(path: string, content: any, encoding?: string): Promise<void>
|
|
5
|
+
uploadFile(params: {
|
|
6
|
+
stream: any
|
|
7
|
+
filename: string
|
|
8
|
+
uploadPath: string
|
|
9
|
+
}): Promise<{ id: string; path: string; size: number }>
|
|
10
|
+
deleteFile(path: string): Promise<void>
|
|
11
|
+
moveFile(params: { source: string; destination: string }): Promise<void>
|
|
12
|
+
|
|
13
|
+
// Directory operations
|
|
14
|
+
readFolders(params: { path: string }): Promise<any[]>
|
|
15
|
+
createDirectory(path: string): Promise<void>
|
|
16
|
+
deleteDirectory(path: string): Promise<void>
|
|
17
|
+
|
|
18
|
+
// File info
|
|
19
|
+
fileExists(path: string): Promise<boolean>
|
|
20
|
+
getFileSize(path: string): Promise<number>
|
|
21
|
+
|
|
22
|
+
// Connection management
|
|
23
|
+
connect(): Promise<void>
|
|
24
|
+
disconnect(): Promise<void>
|
|
25
|
+
isConnected(): boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StorageConfig {
|
|
29
|
+
type: 's3' | 'sftp' | 'ftp' | 'local'
|
|
30
|
+
host?: string
|
|
31
|
+
port?: number
|
|
32
|
+
username?: string
|
|
33
|
+
password?: string
|
|
34
|
+
privateKey?: string
|
|
35
|
+
bucketName?: string
|
|
36
|
+
accessKeyId?: string
|
|
37
|
+
secretAccessKey?: string
|
|
38
|
+
region?: string
|
|
39
|
+
basePath?: string
|
|
40
|
+
timeout?: number
|
|
41
|
+
retries?: number
|
|
42
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { xml2js, js2xml } from 'xml-js'
|
|
2
|
+
|
|
3
|
+
function formatDateYMD(date: Date): string {
|
|
4
|
+
const y = date.getFullYear()
|
|
5
|
+
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
6
|
+
const d = String(date.getDate()).padStart(2, '0')
|
|
7
|
+
return `${y}-${m}-${d}`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeDateLikeValue(value: any): any {
|
|
11
|
+
if (value instanceof Date) {
|
|
12
|
+
return formatDateYMD(value)
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === 'string') {
|
|
15
|
+
// Heuristic: ISO-like or timezone string → format as YYYY-MM-DD
|
|
16
|
+
if (/^(\d{4}-\d{2}-\d{2})([T\s].*)?$/.test(value) || /GMT/.test(value)) {
|
|
17
|
+
const t = new Date(value)
|
|
18
|
+
if (!isNaN(t.getTime())) return formatDateYMD(t)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatAsCSV(data: any[]): string {
|
|
25
|
+
if (!data || data.length === 0) return ''
|
|
26
|
+
const headers = Object.keys(data[0])
|
|
27
|
+
const lines = [headers.join(',')]
|
|
28
|
+
for (const row of data) {
|
|
29
|
+
const line = headers
|
|
30
|
+
.map(h => {
|
|
31
|
+
const raw = row[h]
|
|
32
|
+
const value = normalizeDateLikeValue(raw)
|
|
33
|
+
if (value === null || value === undefined) return ''
|
|
34
|
+
return typeof value === 'string' ? `"${value}"` : value
|
|
35
|
+
})
|
|
36
|
+
.join(',')
|
|
37
|
+
lines.push(line)
|
|
38
|
+
}
|
|
39
|
+
return lines.join('\n')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatAsXML(data: any[], rootName = 'rows', rowName = 'row'): string {
|
|
43
|
+
// Normalize date-like values in a deep-copy manner
|
|
44
|
+
const normalized = data.map(row => {
|
|
45
|
+
const obj: any = {}
|
|
46
|
+
for (const [k, v] of Object.entries(row)) {
|
|
47
|
+
obj[k] = normalizeDateLikeValue(v)
|
|
48
|
+
}
|
|
49
|
+
return obj
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const wrapped = {
|
|
53
|
+
[rootName]: {
|
|
54
|
+
_attributes: {},
|
|
55
|
+
[rowName]: normalized
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return js2xml(wrapped, { compact: true, spaces: 2 })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type SupportedContentType = 'csv' | 'xml' | 'xlsx'
|
|
62
|
+
|
|
63
|
+
export function formatData(data: any[], type: SupportedContentType = 'csv'): string | Buffer {
|
|
64
|
+
switch (type) {
|
|
65
|
+
case 'csv':
|
|
66
|
+
return formatAsCSV(data)
|
|
67
|
+
case 'xml':
|
|
68
|
+
return formatAsXML(data)
|
|
69
|
+
case 'xlsx':
|
|
70
|
+
// TODO: implement excel generation using a lightweight lib if needed
|
|
71
|
+
// returning CSV as a placeholder to avoid new deps
|
|
72
|
+
return formatAsCSV(data)
|
|
73
|
+
default:
|
|
74
|
+
return formatAsCSV(data)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Simple CSV parser (does not handle embedded commas/newlines/escaped quotes)
|
|
79
|
+
export function parseCSV(csvContent: string): any[] {
|
|
80
|
+
const lines = csvContent.trim().split('\n')
|
|
81
|
+
if (lines.length < 2) return []
|
|
82
|
+
const headers = lines[0].split(',').map(h => h.trim())
|
|
83
|
+
const result = []
|
|
84
|
+
for (let i = 1; i < lines.length; i++) {
|
|
85
|
+
const values = lines[i].split(',').map(v => v.trim())
|
|
86
|
+
const row: any = {}
|
|
87
|
+
headers.forEach((header, index) => {
|
|
88
|
+
let value = values[index] || ''
|
|
89
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
90
|
+
value = value.slice(1, -1)
|
|
91
|
+
}
|
|
92
|
+
row[header] = value
|
|
93
|
+
})
|
|
94
|
+
result.push(row)
|
|
95
|
+
}
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
@@ -1,33 +1,95 @@
|
|
|
1
|
-
import '../
|
|
1
|
+
import { StorageManager } from '../storage/storage-manager'
|
|
2
|
+
import { StorageConfig } from '../storage/storage-provider.interface'
|
|
3
|
+
import { logger } from '@things-factory/env'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
export async function generateFiles(params: any[], storageConfig?: StorageConfig) {
|
|
6
|
+
const storageManager = StorageManager.getInstance()
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
// Primary target config (SFTP or caller-provided)
|
|
9
|
+
const primaryConfig: StorageConfig = storageConfig || require('@things-factory/env').config.get('sftpFileStorage')
|
|
10
|
+
// Optional archive config to S3 (uses sftpFileStorage in config when type is 's3')
|
|
11
|
+
const archiveConfig: StorageConfig | null = (() => {
|
|
12
|
+
try {
|
|
13
|
+
const cfg = require('@things-factory/env').config.get('sftpFileStorage')
|
|
14
|
+
return cfg && cfg.type === 's3' ? cfg : null
|
|
15
|
+
} catch (_) {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
})()
|
|
19
|
+
|
|
20
|
+
logger.info(
|
|
21
|
+
`generateFiles: primary=${primaryConfig.type} archive=${
|
|
22
|
+
archiveConfig ? archiveConfig.type + ':' + archiveConfig.bucketName : 'disabled'
|
|
23
|
+
}`
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms))
|
|
6
27
|
|
|
7
|
-
export async function generateFiles(params: any[]) {
|
|
8
|
-
const fileDirectory = './uploaded-files'
|
|
9
28
|
for (let i = 0; i < params.length; i++) {
|
|
10
29
|
let param: any = params[i]
|
|
11
30
|
const { uploadPath, content, title } = param
|
|
12
31
|
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
// Ensure remote directory exists before upload (skip when empty string or undefined)
|
|
33
|
+
if (uploadPath) {
|
|
34
|
+
let attempts = 0
|
|
35
|
+
while (true) {
|
|
36
|
+
try {
|
|
37
|
+
await storageManager.createDirectory(primaryConfig, uploadPath)
|
|
38
|
+
break
|
|
39
|
+
} catch (e) {
|
|
40
|
+
attempts++
|
|
41
|
+
if (attempts >= 3) {
|
|
42
|
+
logger.warn(`createDirectory failed for ${uploadPath}: ${e?.message || e}`)
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
await sleep(200 * attempts)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
15
48
|
}
|
|
16
49
|
|
|
17
|
-
|
|
50
|
+
// Compose remote relative path (under basePath when configured)
|
|
51
|
+
const remotePath = `${uploadPath ? uploadPath + '/' : ''}${title}`.replace(/\/+/, '/').replace(/\/+/, '/')
|
|
18
52
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
53
|
+
// Upload to primary
|
|
54
|
+
if (primaryConfig.type === 'sftp') {
|
|
55
|
+
// Stream upload with retries to reduce connection issues
|
|
56
|
+
const { Readable } = require('stream')
|
|
57
|
+
let attempts = 0
|
|
58
|
+
while (true) {
|
|
59
|
+
try {
|
|
60
|
+
const dataStream = Readable.from([content])
|
|
61
|
+
await storageManager.uploadFile(primaryConfig, {
|
|
62
|
+
stream: dataStream,
|
|
63
|
+
filename: title,
|
|
64
|
+
uploadPath: uploadPath || ''
|
|
65
|
+
})
|
|
66
|
+
break
|
|
67
|
+
} catch (e) {
|
|
68
|
+
attempts++
|
|
69
|
+
if (attempts >= 3) {
|
|
70
|
+
logger.warn(`SFTP upload failed for ${title} to ${uploadPath || '(root)'}: ${e?.message || e}`)
|
|
71
|
+
throw e
|
|
72
|
+
}
|
|
73
|
+
await sleep(300 * attempts)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// Direct write for non-SFTP providers
|
|
78
|
+
await storageManager.writeFile(primaryConfig, remotePath, content, 'utf-8')
|
|
79
|
+
}
|
|
23
80
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
81
|
+
// Optional: archive a copy to S3 under <platform>/
|
|
82
|
+
if (archiveConfig) {
|
|
83
|
+
const platformPrefix: string = (param.platform || '').toString().trim()
|
|
84
|
+
const archiveKey = `${platformPrefix}/${title}`
|
|
85
|
+
logger.info(`generateFiles: archiving to S3 key=${archiveKey}`)
|
|
86
|
+
try {
|
|
87
|
+
await storageManager.writeFile(archiveConfig, archiveKey, content, 'utf-8')
|
|
88
|
+
} catch (e) {
|
|
89
|
+
logger.warn(`S3 archive failed for ${title}: ${e?.message || e}`)
|
|
90
|
+
}
|
|
27
91
|
}
|
|
28
92
|
}
|
|
29
93
|
|
|
30
|
-
fs.rm(fileDirectory, { recursive: true })
|
|
31
|
-
|
|
32
94
|
return true
|
|
33
95
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import '../
|
|
1
|
+
import { StorageManager } from '../storage/storage-manager'
|
|
2
|
+
import { StorageConfig } from '../storage/storage-provider.interface'
|
|
3
|
+
import { getSftpStorageConfig } from '../sftp-const'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const sftpDirectories: any[] = await
|
|
5
|
+
export async function getPermittedDirectories(params: any, context: any, storageConfig?: StorageConfig) {
|
|
6
|
+
const storageManager = StorageManager.getInstance()
|
|
7
|
+
const config = storageConfig || getSftpStorageConfig()
|
|
8
|
+
const sftpDirectories: any[] = await storageManager.readFolders(config, params)
|
|
7
9
|
|
|
8
10
|
return sftpDirectories
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export async function readFile(fileKey: any) {
|
|
12
|
-
const
|
|
13
|
+
export async function readFile(fileKey: any, storageConfig?: StorageConfig) {
|
|
14
|
+
const storageManager = StorageManager.getInstance()
|
|
15
|
+
const config = storageConfig || getSftpStorageConfig()
|
|
16
|
+
const file: any = await storageManager.readFile(config, fileKey, 'utf-8')
|
|
13
17
|
|
|
14
18
|
return file
|
|
15
19
|
}
|