@things-factory/integration-sftp 4.3.660 → 4.3.667
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
|
@@ -60,6 +60,22 @@ export class Sftp {
|
|
|
60
60
|
@Field({ nullable: true })
|
|
61
61
|
username?: string
|
|
62
62
|
|
|
63
|
+
@Column({ nullable: true })
|
|
64
|
+
@Field({ nullable: true })
|
|
65
|
+
schedule?: string
|
|
66
|
+
|
|
67
|
+
// Store password securely; not exposed via GraphQL Field decorator
|
|
68
|
+
@Column({ nullable: true })
|
|
69
|
+
password?: string
|
|
70
|
+
|
|
71
|
+
@Column({ nullable: true })
|
|
72
|
+
@Field({ nullable: true })
|
|
73
|
+
host?: string
|
|
74
|
+
|
|
75
|
+
@Column({ type: 'int', nullable: true })
|
|
76
|
+
@Field({ nullable: true })
|
|
77
|
+
port?: number
|
|
78
|
+
|
|
63
79
|
@Column({
|
|
64
80
|
nullable: true
|
|
65
81
|
})
|
|
@@ -78,6 +94,10 @@ export class Sftp {
|
|
|
78
94
|
@Field({ nullable: true })
|
|
79
95
|
folderPath?: string
|
|
80
96
|
|
|
97
|
+
@Column({ nullable: true })
|
|
98
|
+
@Field({ nullable: true })
|
|
99
|
+
basePath?: string
|
|
100
|
+
|
|
81
101
|
@Column({
|
|
82
102
|
nullable: true
|
|
83
103
|
})
|
|
@@ -111,6 +131,20 @@ export class Sftp {
|
|
|
111
131
|
@Field({ nullable: true })
|
|
112
132
|
responseFileTypes: string
|
|
113
133
|
|
|
134
|
+
@Column({
|
|
135
|
+
nullable: true
|
|
136
|
+
})
|
|
137
|
+
@Field({ nullable: true })
|
|
138
|
+
sftpType: string
|
|
139
|
+
|
|
140
|
+
@Column({ type: 'int', nullable: true })
|
|
141
|
+
@Field({ nullable: true })
|
|
142
|
+
timeout?: number
|
|
143
|
+
|
|
144
|
+
@Column({ type: 'int', nullable: true })
|
|
145
|
+
@Field({ nullable: true })
|
|
146
|
+
retries?: number
|
|
147
|
+
|
|
114
148
|
@Column({ nullable: true })
|
|
115
149
|
@Field({ nullable: true })
|
|
116
150
|
lastTimeSync?: Date
|
package/server/sftp-const.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { config } from '@things-factory/env'
|
|
2
|
+
import { StorageManager } from './storage/storage-manager'
|
|
3
|
+
import { StorageConfig } from './storage/storage-provider.interface'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
// Path constants
|
|
4
6
|
export var SUBMITDATAPATH: string = '/sob/data'
|
|
5
7
|
export var SUCCESSDATAPATH: string = '/sob/success'
|
|
6
8
|
export var FAILEDDATAPATH: string = '/sob/failed'
|
|
@@ -11,3 +13,41 @@ export var SNSUBMITDATAPATH: string = '/sn/data'
|
|
|
11
13
|
export var SNSUCCESSDATAPATH: string = '/sn/success'
|
|
12
14
|
export var SNFAILEDDATAPATH: string = '/sn/failed'
|
|
13
15
|
export var BACKUPPATH: string = 'hatiosea/'
|
|
16
|
+
|
|
17
|
+
// Storage configuration helper
|
|
18
|
+
export function getSftpStorageConfig(): StorageConfig {
|
|
19
|
+
return config.get('sftpFileStorage')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Legacy compatibility - deprecated, use StorageManager instead
|
|
23
|
+
export var SFTPFILESTORAGE: any = {
|
|
24
|
+
// This is kept for backward compatibility but should not be used in new code
|
|
25
|
+
// Use StorageManager.getInstance() instead
|
|
26
|
+
get config() {
|
|
27
|
+
return getSftpStorageConfig()
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async readFile(path: string, encoding?: string) {
|
|
31
|
+
const storageManager = StorageManager.getInstance()
|
|
32
|
+
const storageConfig = getSftpStorageConfig()
|
|
33
|
+
return await storageManager.readFile(storageConfig, path, encoding)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async uploadFile(params: { stream: any; filename: string; uploadPath: string }) {
|
|
37
|
+
const storageManager = StorageManager.getInstance()
|
|
38
|
+
const storageConfig = getSftpStorageConfig()
|
|
39
|
+
return await storageManager.uploadFile(storageConfig, params)
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async readFolders(params: { path: string }) {
|
|
43
|
+
const storageManager = StorageManager.getInstance()
|
|
44
|
+
const storageConfig = getSftpStorageConfig()
|
|
45
|
+
return await storageManager.readFolders(storageConfig, params)
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async moveFile(params: { source: string; destination: string }) {
|
|
49
|
+
const storageManager = StorageManager.getInstance()
|
|
50
|
+
const storageConfig = getSftpStorageConfig()
|
|
51
|
+
return await storageManager.moveFile(storageConfig, params)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as ftp from 'basic-ftp'
|
|
2
|
+
import { FileType } from 'basic-ftp'
|
|
3
|
+
import { StorageProvider, StorageConfig } from '../storage-provider.interface'
|
|
4
|
+
import { logger } from '@things-factory/env'
|
|
5
|
+
|
|
6
|
+
export class FtpStorageProvider implements StorageProvider {
|
|
7
|
+
private client: ftp.Client
|
|
8
|
+
private config: StorageConfig
|
|
9
|
+
private connected: boolean = false
|
|
10
|
+
|
|
11
|
+
constructor(config: StorageConfig) {
|
|
12
|
+
this.config = config
|
|
13
|
+
this.client = new ftp.Client()
|
|
14
|
+
this.client.ftp.verbose = false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async connect(): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
await this.client.access({
|
|
20
|
+
host: this.config.host!,
|
|
21
|
+
port: this.config.port || 21,
|
|
22
|
+
user: this.config.username!,
|
|
23
|
+
password: this.config.password!,
|
|
24
|
+
secure: false // Set to true for FTPS
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
this.connected = true
|
|
28
|
+
logger.info('FTP Storage Provider connected successfully')
|
|
29
|
+
} catch (error) {
|
|
30
|
+
this.connected = false
|
|
31
|
+
throw new Error(`Failed to connect to FTP: ${error}`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async disconnect(): Promise<void> {
|
|
36
|
+
if (this.connected) {
|
|
37
|
+
this.client.close()
|
|
38
|
+
this.connected = false
|
|
39
|
+
logger.info('FTP Storage Provider disconnected')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
isConnected(): boolean {
|
|
44
|
+
return this.connected
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async readFile(path: string, encoding?: string): Promise<any> {
|
|
48
|
+
const fullPath = this.getFullPath(path)
|
|
49
|
+
|
|
50
|
+
// Create a temporary file to download to
|
|
51
|
+
const tempFile = `/tmp/temp_${Date.now()}.tmp`
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await this.client.downloadTo(tempFile, fullPath)
|
|
55
|
+
const fs = require('fs')
|
|
56
|
+
const content = fs.readFileSync(tempFile)
|
|
57
|
+
fs.unlinkSync(tempFile) // Clean up temp file
|
|
58
|
+
|
|
59
|
+
if (encoding) {
|
|
60
|
+
return content.toString(encoding as any)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return content
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Clean up temp file if it exists
|
|
66
|
+
const fs = require('fs')
|
|
67
|
+
if (fs.existsSync(tempFile)) {
|
|
68
|
+
fs.unlinkSync(tempFile)
|
|
69
|
+
}
|
|
70
|
+
throw error
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async writeFile(path: string, content: any, encoding?: string): Promise<void> {
|
|
75
|
+
const fullPath = this.getFullPath(path)
|
|
76
|
+
|
|
77
|
+
if (typeof content === 'string') {
|
|
78
|
+
await this.client.uploadFrom(content, fullPath)
|
|
79
|
+
} else {
|
|
80
|
+
// Convert to string if it's a buffer or other type
|
|
81
|
+
const contentString = content.toString(encoding || 'utf8')
|
|
82
|
+
await this.client.uploadFrom(contentString, fullPath)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async uploadFile(params: {
|
|
87
|
+
stream: any
|
|
88
|
+
filename: string
|
|
89
|
+
uploadPath: string
|
|
90
|
+
}): Promise<{ id: string; path: string; size: number }> {
|
|
91
|
+
const fullPath = this.getFullPath(`${params.uploadPath}/${params.filename}`)
|
|
92
|
+
let size = 0
|
|
93
|
+
|
|
94
|
+
return new Promise<{ id: string; path: string; size: number }>((resolve, reject) => {
|
|
95
|
+
params.stream
|
|
96
|
+
.on('data', (chunk: any) => (size += chunk.length))
|
|
97
|
+
.on('error', (error: any) => reject(error))
|
|
98
|
+
.on('end', async () => {
|
|
99
|
+
try {
|
|
100
|
+
await this.client.uploadFrom(params.stream, fullPath)
|
|
101
|
+
resolve({
|
|
102
|
+
id: Date.now().toString(),
|
|
103
|
+
path: params.uploadPath,
|
|
104
|
+
size
|
|
105
|
+
})
|
|
106
|
+
} catch (error) {
|
|
107
|
+
reject(error)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async deleteFile(path: string): Promise<void> {
|
|
114
|
+
const fullPath = this.getFullPath(path)
|
|
115
|
+
await this.client.remove(fullPath)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async moveFile(params: { source: string; destination: string }): Promise<void> {
|
|
119
|
+
const sourcePath = this.getFullPath(params.source)
|
|
120
|
+
const destPath = this.getFullPath(params.destination)
|
|
121
|
+
|
|
122
|
+
// FTP doesn't have native move, so we copy then delete
|
|
123
|
+
const tempFile = `/tmp/temp_move_${Date.now()}.tmp`
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await this.client.downloadTo(tempFile, sourcePath)
|
|
127
|
+
await this.client.uploadFrom(tempFile, destPath)
|
|
128
|
+
await this.client.remove(sourcePath)
|
|
129
|
+
} finally {
|
|
130
|
+
// Clean up temp file
|
|
131
|
+
const fs = require('fs')
|
|
132
|
+
if (fs.existsSync(tempFile)) {
|
|
133
|
+
fs.unlinkSync(tempFile)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async readFolders(params: { path: string }): Promise<any[]> {
|
|
139
|
+
const fullPath = this.getFullPath(params.path)
|
|
140
|
+
const list = await this.client.list(fullPath)
|
|
141
|
+
|
|
142
|
+
// Filter for files only, checking if it's not a directory
|
|
143
|
+
return list.filter(item => item.type !== FileType.Directory)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async createDirectory(path: string): Promise<void> {
|
|
147
|
+
const fullPath = this.getFullPath(path)
|
|
148
|
+
await this.client.ensureDir(fullPath)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async deleteDirectory(path: string): Promise<void> {
|
|
152
|
+
const fullPath = this.getFullPath(path)
|
|
153
|
+
await this.client.removeDir(fullPath)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async fileExists(path: string): Promise<boolean> {
|
|
157
|
+
try {
|
|
158
|
+
const fullPath = this.getFullPath(path)
|
|
159
|
+
await this.client.size(fullPath)
|
|
160
|
+
return true
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async getFileSize(path: string): Promise<number> {
|
|
167
|
+
const fullPath = this.getFullPath(path)
|
|
168
|
+
return await this.client.size(fullPath)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private getFullPath(path: string): string {
|
|
172
|
+
if (this.config.basePath) {
|
|
173
|
+
return `${this.config.basePath}/${path}`.replace(/\/+/g, '/')
|
|
174
|
+
}
|
|
175
|
+
return path
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import { StorageProvider, StorageConfig } from '../storage-provider.interface'
|
|
4
|
+
import { logger } from '@things-factory/env'
|
|
5
|
+
|
|
6
|
+
export class LocalStorageProvider implements StorageProvider {
|
|
7
|
+
private config: StorageConfig
|
|
8
|
+
private connected: boolean = false
|
|
9
|
+
|
|
10
|
+
constructor(config: StorageConfig) {
|
|
11
|
+
this.config = config
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async connect(): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
const basePath = this.config.basePath || './storage'
|
|
17
|
+
|
|
18
|
+
// Ensure base directory exists
|
|
19
|
+
if (!fs.existsSync(basePath)) {
|
|
20
|
+
fs.mkdirSync(basePath, { recursive: true })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.connected = true
|
|
24
|
+
logger.info('Local Storage Provider connected successfully')
|
|
25
|
+
} catch (error) {
|
|
26
|
+
this.connected = false
|
|
27
|
+
throw new Error(`Failed to connect to Local Storage: ${error}`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async disconnect(): Promise<void> {
|
|
32
|
+
this.connected = false
|
|
33
|
+
logger.info('Local Storage Provider disconnected')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isConnected(): boolean {
|
|
37
|
+
return this.connected
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async readFile(filePath: string, encoding?: string): Promise<any> {
|
|
41
|
+
const fullPath = this.getFullPath(filePath)
|
|
42
|
+
|
|
43
|
+
if (encoding) {
|
|
44
|
+
return fs.readFileSync(fullPath, encoding as BufferEncoding)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return fs.readFileSync(fullPath)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async writeFile(filePath: string, content: any, encoding?: string): Promise<void> {
|
|
51
|
+
const fullPath = this.getFullPath(filePath)
|
|
52
|
+
|
|
53
|
+
// Ensure directory exists
|
|
54
|
+
const dir = path.dirname(fullPath)
|
|
55
|
+
if (!fs.existsSync(dir)) {
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (encoding) {
|
|
60
|
+
fs.writeFileSync(fullPath, content, encoding as BufferEncoding)
|
|
61
|
+
} else {
|
|
62
|
+
fs.writeFileSync(fullPath, content)
|
|
63
|
+
}
|
|
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
|
+
|
|
73
|
+
// Ensure directory exists
|
|
74
|
+
const dir = path.dirname(fullPath)
|
|
75
|
+
if (!fs.existsSync(dir)) {
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let size = 0
|
|
80
|
+
|
|
81
|
+
return new Promise<{ id: string; path: string; size: number }>((resolve, reject) => {
|
|
82
|
+
const writeStream = fs.createWriteStream(fullPath)
|
|
83
|
+
|
|
84
|
+
params.stream
|
|
85
|
+
.pipe(writeStream)
|
|
86
|
+
.on('error', (error: any) => reject(error))
|
|
87
|
+
.on('data', (chunk: any) => (size += chunk.length))
|
|
88
|
+
.on('finish', () => {
|
|
89
|
+
resolve({
|
|
90
|
+
id: Date.now().toString(),
|
|
91
|
+
path: params.uploadPath,
|
|
92
|
+
size
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async deleteFile(filePath: string): Promise<void> {
|
|
99
|
+
const fullPath = this.getFullPath(filePath)
|
|
100
|
+
fs.unlinkSync(fullPath)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async moveFile(params: { source: string; destination: string }): Promise<void> {
|
|
104
|
+
const sourcePath = this.getFullPath(params.source)
|
|
105
|
+
const destPath = this.getFullPath(params.destination)
|
|
106
|
+
|
|
107
|
+
// Ensure destination directory exists
|
|
108
|
+
const dir = path.dirname(destPath)
|
|
109
|
+
if (!fs.existsSync(dir)) {
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fs.renameSync(sourcePath, destPath)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async readFolders(params: { path: string }): Promise<any[]> {
|
|
117
|
+
const fullPath = this.getFullPath(params.path)
|
|
118
|
+
|
|
119
|
+
if (!fs.existsSync(fullPath)) {
|
|
120
|
+
return []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const items = fs.readdirSync(fullPath, { withFileTypes: true })
|
|
124
|
+
return items
|
|
125
|
+
.filter(item => item.isFile())
|
|
126
|
+
.map(item => ({
|
|
127
|
+
Key: path.join(params.path, item.name),
|
|
128
|
+
Size: fs.statSync(path.join(fullPath, item.name)).size
|
|
129
|
+
}))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async createDirectory(dirPath: string): Promise<void> {
|
|
133
|
+
const fullPath = this.getFullPath(dirPath)
|
|
134
|
+
fs.mkdirSync(fullPath, { recursive: true })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async deleteDirectory(dirPath: string): Promise<void> {
|
|
138
|
+
const fullPath = this.getFullPath(dirPath)
|
|
139
|
+
fs.rmSync(fullPath, { recursive: true, force: true })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async fileExists(filePath: string): Promise<boolean> {
|
|
143
|
+
const fullPath = this.getFullPath(filePath)
|
|
144
|
+
return fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getFileSize(filePath: string): Promise<number> {
|
|
148
|
+
const fullPath = this.getFullPath(filePath)
|
|
149
|
+
const stats = fs.statSync(fullPath)
|
|
150
|
+
return stats.size
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private getFullPath(filePath: string): string {
|
|
154
|
+
if (this.config.basePath) {
|
|
155
|
+
return path.resolve(this.config.basePath, filePath)
|
|
156
|
+
}
|
|
157
|
+
return path.resolve(filePath)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import AWS from 'aws-sdk'
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
3
|
+
import { StorageProvider, StorageConfig } from '../storage-provider.interface'
|
|
4
|
+
import { logger } from '@things-factory/env'
|
|
5
|
+
|
|
6
|
+
const { PassThrough } = require('stream')
|
|
7
|
+
|
|
8
|
+
export class S3StorageProvider implements StorageProvider {
|
|
9
|
+
private s3: AWS.S3
|
|
10
|
+
private config: StorageConfig
|
|
11
|
+
private connected: boolean = false
|
|
12
|
+
|
|
13
|
+
constructor(config: StorageConfig) {
|
|
14
|
+
this.config = config
|
|
15
|
+
this.s3 = new AWS.S3({
|
|
16
|
+
accessKeyId: config.accessKeyId,
|
|
17
|
+
secretAccessKey: config.secretAccessKey,
|
|
18
|
+
region: config.region
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async connect(): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
// Assume client ready; defer validation to first real operation (write/read)
|
|
25
|
+
this.connected = true
|
|
26
|
+
logger.info('S3 Storage Provider ready')
|
|
27
|
+
} catch (error) {
|
|
28
|
+
this.connected = false
|
|
29
|
+
throw new Error(`Failed to initialize S3 client: ${error}`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async disconnect(): Promise<void> {
|
|
34
|
+
this.connected = false
|
|
35
|
+
logger.info('S3 Storage Provider disconnected')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
isConnected(): boolean {
|
|
39
|
+
return this.connected
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async readFile(path: string, encoding?: string): Promise<any> {
|
|
43
|
+
const result = await this.s3
|
|
44
|
+
.getObject({
|
|
45
|
+
Bucket: this.config.bucketName!,
|
|
46
|
+
Key: path
|
|
47
|
+
})
|
|
48
|
+
.promise()
|
|
49
|
+
|
|
50
|
+
const body = result.Body
|
|
51
|
+
|
|
52
|
+
if (encoding) {
|
|
53
|
+
return body.toString(encoding as BufferEncoding)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return body
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async writeFile(path: string, content: any, encoding?: string): Promise<void> {
|
|
60
|
+
let body: Buffer
|
|
61
|
+
|
|
62
|
+
if (typeof content === 'string' && encoding) {
|
|
63
|
+
body = Buffer.from(content, encoding as any)
|
|
64
|
+
} else {
|
|
65
|
+
body = Buffer.from(content)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const putParams: AWS.S3.PutObjectRequest = {
|
|
69
|
+
Bucket: this.config.bucketName!,
|
|
70
|
+
Key: path,
|
|
71
|
+
Body: body
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Optional SSE if configured via env/config
|
|
75
|
+
const sse = (this.config as any).serverSideEncryption
|
|
76
|
+
const kmsKeyId = (this.config as any).sseKmsKeyId
|
|
77
|
+
if (sse) {
|
|
78
|
+
putParams.ServerSideEncryption = sse
|
|
79
|
+
if (sse === 'aws:kms' && kmsKeyId) {
|
|
80
|
+
putParams.SSEKMSKeyId = kmsKeyId
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await this.s3.putObject(putParams).promise()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async uploadFile(params: {
|
|
88
|
+
stream: any
|
|
89
|
+
filename: string
|
|
90
|
+
uploadPath: string
|
|
91
|
+
}): Promise<{ id: string; path: string; size: number }> {
|
|
92
|
+
const id = uuidv4()
|
|
93
|
+
let size: number = 0
|
|
94
|
+
|
|
95
|
+
return new Promise<{ id: string; path: string; size: number }>((resolve, reject) =>
|
|
96
|
+
params.stream
|
|
97
|
+
.pipe(
|
|
98
|
+
(() => {
|
|
99
|
+
var pass = new PassThrough()
|
|
100
|
+
|
|
101
|
+
this.s3.upload(
|
|
102
|
+
{
|
|
103
|
+
Bucket: this.config.bucketName!,
|
|
104
|
+
Key: params.uploadPath + '/' + params.filename,
|
|
105
|
+
Body: pass,
|
|
106
|
+
ServerSideEncryption: (this.config as any).serverSideEncryption,
|
|
107
|
+
SSEKMSKeyId: (this.config as any).sseKmsKeyId
|
|
108
|
+
},
|
|
109
|
+
(err, data) => (err ? reject(err) : resolve({ id, path: params.uploadPath, size }))
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return pass
|
|
113
|
+
})()
|
|
114
|
+
)
|
|
115
|
+
.on('error', error => reject(error))
|
|
116
|
+
.on('data', chunk => (size += chunk.length))
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async deleteFile(path: string): Promise<void> {
|
|
121
|
+
await this.s3
|
|
122
|
+
.deleteObject({
|
|
123
|
+
Bucket: this.config.bucketName!,
|
|
124
|
+
Key: path
|
|
125
|
+
})
|
|
126
|
+
.promise()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async moveFile(params: { source: string; destination: string }): Promise<void> {
|
|
130
|
+
try {
|
|
131
|
+
// Copy file to new location
|
|
132
|
+
await this.s3
|
|
133
|
+
.copyObject({
|
|
134
|
+
Bucket: this.config.bucketName!,
|
|
135
|
+
CopySource: this.config.bucketName + '/' + params.source,
|
|
136
|
+
Key: params.destination
|
|
137
|
+
})
|
|
138
|
+
.promise()
|
|
139
|
+
|
|
140
|
+
// Delete original file
|
|
141
|
+
await this.deleteFile(params.source)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new Error(`Failed to move file: ${error}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async readFolders(params: { path: string }): Promise<any[]> {
|
|
148
|
+
const s3Params = {
|
|
149
|
+
Bucket: this.config.bucketName!,
|
|
150
|
+
Delimiter: '/',
|
|
151
|
+
Prefix: params.path
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result = await this.s3.listObjects(s3Params).promise()
|
|
155
|
+
return result.Contents || []
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async createDirectory(path: string): Promise<void> {
|
|
159
|
+
// S3 doesn't have real directories, but we can create an empty object
|
|
160
|
+
await this.s3
|
|
161
|
+
.putObject({
|
|
162
|
+
Bucket: this.config.bucketName!,
|
|
163
|
+
Key: path.endsWith('/') ? path : path + '/',
|
|
164
|
+
Body: ''
|
|
165
|
+
})
|
|
166
|
+
.promise()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async deleteDirectory(path: string): Promise<void> {
|
|
170
|
+
// List all objects with the prefix and delete them
|
|
171
|
+
const objects = await this.s3
|
|
172
|
+
.listObjects({
|
|
173
|
+
Bucket: this.config.bucketName!,
|
|
174
|
+
Prefix: path
|
|
175
|
+
})
|
|
176
|
+
.promise()
|
|
177
|
+
|
|
178
|
+
if (objects.Contents && objects.Contents.length > 0) {
|
|
179
|
+
const deleteParams = {
|
|
180
|
+
Bucket: this.config.bucketName!,
|
|
181
|
+
Delete: {
|
|
182
|
+
Objects: objects.Contents.map(obj => ({ Key: obj.Key! }))
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await this.s3.deleteObjects(deleteParams).promise()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async fileExists(path: string): Promise<boolean> {
|
|
191
|
+
try {
|
|
192
|
+
await this.s3
|
|
193
|
+
.headObject({
|
|
194
|
+
Bucket: this.config.bucketName!,
|
|
195
|
+
Key: path
|
|
196
|
+
})
|
|
197
|
+
.promise()
|
|
198
|
+
return true
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async getFileSize(path: string): Promise<number> {
|
|
205
|
+
const result = await this.s3
|
|
206
|
+
.headObject({
|
|
207
|
+
Bucket: this.config.bucketName!,
|
|
208
|
+
Key: path
|
|
209
|
+
})
|
|
210
|
+
.promise()
|
|
211
|
+
|
|
212
|
+
return result.ContentLength || 0
|
|
213
|
+
}
|
|
214
|
+
}
|