@dnax/core 0.77.9-alpha-1 → 0.78.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.
package/app/hono.ts CHANGED
@@ -14,7 +14,7 @@ import { serveStatic, getConnInfo } from "hono/bun";
14
14
  import { consola } from "consola";
15
15
 
16
16
  import { cors } from "hono/cors";
17
- import { asyncLocalStorage, sessionStorage,requestStorage } from "../lib/asyncLocalStorage";
17
+ import { asyncLocalStorage, sessionStorage, requestStorage } from "../lib/asyncLocalStorage";
18
18
  import { crypt } from "../lib/crypto";
19
19
  import { plugins } from "../lib/plugins";
20
20
  import {
@@ -305,7 +305,7 @@ function HonoInstance(): typeof app {
305
305
  ?.get("content-type")
306
306
  ?.match(/(multipart\/form-data)/) ||
307
307
  c.req?.raw?.headers.get("content-type") ==
308
- "application/x-www-form-urlencoded"
308
+ "application/x-www-form-urlencoded"
309
309
  ) {
310
310
  parseBody =
311
311
  (await c?.req.parseBody({
@@ -818,7 +818,7 @@ function HonoInstance(): typeof app {
818
818
  if (Cfg?.api?.onError && typeof Cfg?.api?.onError == "function") {
819
819
  try {
820
820
  await Cfg?.api?.onError(errorFormated, err);
821
- } catch (e) {}
821
+ } catch (e) { }
822
822
  }
823
823
  return c.json(errorFormated);
824
824
  }
package/app/index.ts CHANGED
@@ -92,7 +92,7 @@ async function runApp(config?: configRunApp, clb?: Function): Promise<Server> {
92
92
  });
93
93
  info += `\n\n`;
94
94
  info += `🔄 ${new Date().toLocaleString()}`.gray;
95
-
95
+
96
96
  if (clb) clb();
97
97
 
98
98
  console.log();
package/index.ts CHANGED
@@ -6,13 +6,12 @@ import * as v from "joi";
6
6
  import { $ } from "bun";
7
7
  import define from "./define";
8
8
  import * as utils from "./utils";
9
- import { app } from "./app/hono";
10
9
  import { FilesystemSftpAdapter, MinioAdapter } from "./lib/media";
11
10
  import { crypt } from "./lib/crypto";
12
11
  import { plugins } from "./lib/plugins";
13
12
  import { contextStorage } from "./lib/asyncLocalStorage";
14
13
  import { Cron as Task } from "croner";
15
- import { serveStatic } from "hono/bun";
14
+ //import { serveStatic } from "hono/bun";
16
15
  import { useDatabaseTools } from "./driver/mongo/database";
17
16
  import { useCache } from "./lib/bento";
18
17
  // Adapter
package/lib/media/sftp.ts CHANGED
@@ -1,116 +1,670 @@
1
- import { Client } from "ssh2";
2
- import type { SFTPWrapper } from "ssh2";
3
- import { consola } from "consola";
1
+ import { $ } from "bun";
2
+ import { join } from "path";
3
+ import { tmpdir } from "os";
4
4
  import path from "path";
5
5
  import chokidar from "chokidar";
6
6
  import fs from "fs-extra";
7
7
  import type { Collection } from "../../types";
8
+ import consola from "consola";
8
9
  const BASE_DIR = "/uploads/";
9
- type ConfigType = {
10
- host: string;
11
- port?: number;
12
- username: string;
13
- password: string;
14
- remoteDir: string;
15
- path?: string;
16
- ignorePatterns?: string[];
17
- //privateKeyPath?: string;
18
- };
10
+
11
+ // ============================================================================
12
+ // TYPES & INTERFACES
13
+ // ============================================================================
14
+
15
+ interface SftpConfig {
16
+ host: string;
17
+ user: string;
18
+ username?: string;
19
+ port?: number;
20
+ password?: string;
21
+ identityFile?: string;
22
+ remoteDir?: string;
23
+ ignorePatterns?: string[];
24
+ }
25
+
26
+ interface FileInfo {
27
+ name: string;
28
+ size: number;
29
+ isDirectory: boolean;
30
+ modified: Date;
31
+ permissions: string;
32
+ }
33
+
34
+ interface UploadOptions {
35
+ onProgress?: (percent: number) => void;
36
+ overwrite?: boolean;
37
+ }
38
+
39
+ interface DownloadOptions {
40
+ onProgress?: (percent: number) => void;
41
+ }
42
+
43
+ // ============================================================================
44
+ // FILESYSTEM SFTP ADAPTER
45
+ // ============================================================================
46
+
19
47
  class FilesystemSftpAdapter {
20
- conn: Client;
21
- type: "sftp";
22
- config: ConfigType;
23
- constructor(
24
- config = {
25
- port: 22,
26
- } as ConfigType
27
- ) {
28
- this.config = config;
29
- this.type = "sftp";
30
- this.conn = new Client({
31
- captureRejections: true,
32
- //captureRejections: true,
33
- });
34
-
35
- this.connect()
36
- .then((e) => {
37
- e.conn.end();
38
- })
39
- .catch((err) => {
40
- consola.error(`SFTP: Failed to connect ${this.config.host}`.red);
41
- });
42
- // this.conn.
43
- return this;
44
- }
48
+ private config: SftpConfig;
49
+ private connected: boolean = false;
50
+ private remoteDir: string;
51
+ private ignorePatterns: string[];
52
+ type: "sftp";
45
53
 
46
- async syncCollectionMedia(col: Collection) {
47
- if (!this.config?.remoteDir) this.config.remoteDir = "/home/" + col?.slug;
48
-
49
- let visibility = col?.media?.visibility || "public";
50
- let dirToWatch = path.join(process.cwd(), BASE_DIR, col?.slug, visibility);
51
- dirToWatch = path.resolve(dirToWatch);
52
- const watcher = chokidar.watch(dirToWatch, {
53
- persistent: true,
54
- ignoreInitial: true,
55
- ignored: this.config.ignorePatterns || [],
56
- });
57
-
58
- watcher.on("all", (event, filePath) => {
59
- if (event == "change" || event == "add") {
60
- //consola.info(`SFTP: ${event} ${filePath}`);
61
- let remotePath = path.join(
62
- this.config.remoteDir,
63
- path.basename(filePath)
64
- );
65
- this.put(filePath, remotePath);
66
- }
67
- });
68
- }
54
+ constructor(config: SftpConfig) {
55
+ this.type = "sftp";
56
+ if (!config.username) config.username = "root";
57
+ config.user = config.username;
58
+ this.config = config;
59
+ this.remoteDir = config.remoteDir || "/";
60
+ this.ignorePatterns = config.ignorePatterns || [];
61
+ this.connect()
62
+ }
63
+
64
+ // --------------------------------------------------------------------------
65
+ // PRIVATE HELPERS
66
+ // --------------------------------------------------------------------------
67
+
68
+ private get destination(): string {
69
+ return `${this.config.user}@${this.config.host}`;
70
+ }
71
+
72
+ private resolvePath(remotePath: string): string {
73
+ if (remotePath.startsWith("/")) {
74
+ return remotePath;
75
+ }
76
+ return join(this.remoteDir, remotePath).replace(/\\/g, "/");
77
+ }
78
+
79
+ private shouldIgnore(filePath: string): boolean {
80
+ if (this.ignorePatterns.length === 0) return false;
81
+
82
+ const fileName = filePath.split("/").pop() || filePath;
69
83
 
70
- connect(): Promise<{ sftp: SFTPWrapper; conn: Client }> {
71
- return new Promise((resolve, reject) => {
72
- this.conn
73
- .on("ready", () => {
74
- this.conn.sftp((err, sftp) => {
75
- if (err) {
76
- this.conn.end();
77
- return reject(err?.message);
84
+ for (const pattern of this.ignorePatterns) {
85
+ // Support glob patterns: *, **, ?
86
+ const regexPattern = pattern
87
+ .replace(/\./g, "\\.")
88
+ .replace(/\*\*/g, "{{DOUBLE_STAR}}")
89
+ .replace(/\*/g, "[^/]*")
90
+ .replace(/{{DOUBLE_STAR}}/g, ".*")
91
+ .replace(/\?/g, ".");
92
+
93
+ const regex = new RegExp(`^${regexPattern}$`);
94
+
95
+ if (regex.test(fileName) || regex.test(filePath)) {
96
+ return true;
97
+ }
98
+ }
99
+
100
+ return false;
101
+ }
102
+
103
+ private async getLocalFiles(localDir: string, baseDir = ""): Promise<string[]> {
104
+ const files: string[] = [];
105
+ const entries = await $`ls -1 ${localDir}`.quiet().text();
106
+
107
+ for (const entry of entries.trim().split("\n").filter(Boolean)) {
108
+ const fullPath = join(localDir, entry);
109
+ const relativePath = baseDir ? `${baseDir}/${entry}` : entry;
110
+
111
+ if (this.shouldIgnore(relativePath) || this.shouldIgnore(entry)) {
112
+ continue;
78
113
  }
79
- resolve({
80
- sftp: sftp,
81
- conn: this.conn,
82
- });
83
- });
84
- })
85
- .on("error", (err) => reject(err?.message))
86
- .connect(this.config);
87
- });
114
+
115
+ const isDir = await $`test -d ${fullPath} && echo yes || echo no`.quiet().text();
116
+
117
+ if (isDir.trim() === "yes") {
118
+ const subFiles = await this.getLocalFiles(fullPath, relativePath);
119
+ files.push(...subFiles);
120
+ } else {
121
+ files.push(relativePath);
122
+ }
123
+ }
124
+
125
+ return files;
126
+ }
127
+
128
+ private get portArg(): string {
129
+ return this.config.port ? `-P ${this.config.port}` : "";
130
+ }
131
+
132
+ private get sshPortArg(): string {
133
+ return this.config.port ? `-p ${this.config.port}` : "";
134
+ }
135
+
136
+ private get identityArg(): string {
137
+ if (!this.config.identityFile) return "";
138
+ const keyPath = this.config.identityFile.replace("~", process.env.HOME || "");
139
+ return `-i ${keyPath}`;
140
+ }
141
+
142
+ private async execWithPassword(command: string): Promise<string> {
143
+ const { password } = this.config;
144
+
145
+ if (!password) {
146
+ const result = await $`sh -c ${command}`.quiet();
147
+ return result.text();
148
+ }
149
+
150
+ const expectScript = `
151
+ set timeout 30
152
+ spawn sh -c "${command.replace(/"/g, '\\"')}"
153
+ expect {
154
+ "password:" {
155
+ send "${password}\\r"
156
+ exp_continue
157
+ }
158
+ "Password:" {
159
+ send "${password}\\r"
160
+ exp_continue
88
161
  }
89
- async put(localPath: string, remotePath: string) {
90
- let { sftp, conn } = await this.connect();
91
- return new Promise(async (resolve, reject) => {
92
- sftp.fastPut(
93
- localPath,
94
- remotePath,
95
- {
96
-
97
- mode: 0o777,
98
- // concurrency
99
- },
100
- (err: any) => {
101
- conn.end();
102
- if (err) {
103
- console.error(err?.message || err);
104
- }
105
- if (err) {
106
- reject(err?.message);
107
- }
108
-
109
- resolve(true);
110
- }
111
- );
112
- });
162
+ "yes/no" {
163
+ send "yes\\r"
164
+ exp_continue
113
165
  }
166
+ eof
114
167
  }
168
+ catch wait result
169
+ exit [lindex $result 3]
170
+ `;
171
+
172
+ const scriptFile = join(tmpdir(), `expect-${Date.now()}.exp`);
173
+ await Bun.write(scriptFile, expectScript);
174
+
175
+ try {
176
+ const result = await $`expect ${scriptFile}`.quiet();
177
+ return result.text();
178
+ } finally {
179
+ await $`rm -f ${scriptFile}`.quiet().nothrow();
180
+ }
181
+ }
182
+
183
+ private async sshExec(command: string): Promise<string> {
184
+ const cmd = `ssh ${this.identityArg} ${this.sshPortArg} -o StrictHostKeyChecking=accept-new ${this.destination} ${command}`;
185
+ return await this.execWithPassword(cmd);
186
+ }
187
+
188
+ private cleanOutput(output: string): string[] {
189
+ return output
190
+ .split("\n")
191
+ .filter((line) => {
192
+ const lower = line.toLowerCase();
193
+ return (
194
+ line.trim() &&
195
+ !lower.includes("password") &&
196
+ !lower.includes("spawn") &&
197
+ !lower.includes("expect")
198
+ );
199
+ })
200
+ .map((line) => line.trim());
201
+ }
202
+
203
+ // --------------------------------------------------------------------------
204
+ // CONNECTION
205
+ // --------------------------------------------------------------------------
206
+
207
+ async connect(): Promise<void> {
208
+ try {
209
+ // consola.info(`Connecting to ${this.destination}`.blue);
210
+ const cmd = `ssh ${this.identityArg} ${this.sshPortArg} -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 ${this.destination} echo connected`;
211
+ const output = await this.execWithPassword(cmd);
212
+
213
+ if (!output.includes("connected")) {
214
+ throw new Error("Connection verification failed");
215
+ }
216
+
217
+ this.connected = true;
218
+ consola.log(`✓ SFTP Connected to ${this.config.host}`.green);
219
+ } catch (error: any) {
220
+ // throw new Error(`Connexion échouée: ${error?.message}`);
221
+ consola.error(`Connexion échouée: ${error?.message}`);
222
+ }
223
+ }
224
+
225
+ async disconnect(): Promise<void> {
226
+ this.connected = false;
227
+ console.log(`✓ Déconnecté de ${this.config.host}`);
228
+ }
229
+
230
+ isConnected(): boolean {
231
+ return this.connected;
232
+ }
233
+
234
+ // --------------------------------------------------------------------------
235
+ // UPLOAD
236
+ // --------------------------------------------------------------------------
237
+
238
+ async syncCollectionMedia(col: Collection) {
239
+ if (!this.config?.remoteDir) this.config.remoteDir = "/home/" + col?.slug;
240
+
241
+ let visibility = col?.media?.visibility || "public";
242
+ let dirToWatch = path.join(process.cwd(), BASE_DIR, col?.slug, visibility);
243
+ dirToWatch = path.resolve(dirToWatch);
244
+ const watcher = chokidar.watch(dirToWatch, {
245
+ persistent: true,
246
+ ignoreInitial: true,
247
+ ignored: this.config.ignorePatterns || [],
248
+ });
249
+
250
+ watcher.on("all", (event, filePath) => {
251
+ // console.log(`SFTP: ${event} ${filePath}`);
252
+ if (event == "change" || event == "add") {
253
+ //consola.info(`SFTP: ${event} ${filePath}`);
254
+ let remotePath = path.join(
255
+ this.config.remoteDir || '/home/' + col?.slug,
256
+ path.basename(filePath)
257
+ );
258
+ this.upload(filePath, remotePath).catch(err => {
259
+ consola.error(`${err?.message}`.red);
260
+ });
261
+ }
262
+ });
263
+ }
264
+ async upload(
265
+ localPath: string,
266
+ remotePath: string,
267
+ options?: UploadOptions
268
+ ): Promise<void> {
269
+ const file = Bun.file(localPath);
270
+
271
+ if (!(await file.exists())) {
272
+ throw new Error(`Fichier local introuvable: ${localPath}`);
273
+ }
274
+
275
+ const fileName = localPath.split("/").pop() || localPath;
276
+ if (this.shouldIgnore(fileName) || this.shouldIgnore(localPath)) {
277
+ console.log(`⊘ Ignoré: ${localPath}`);
278
+ return;
279
+ }
280
+
281
+ const resolvedPath = this.resolvePath(remotePath);
282
+
283
+ if (!options?.overwrite) {
284
+ const exists = await this.exists(resolvedPath);
285
+ if (exists) {
286
+ throw new Error(`Fichier distant existe déjà: ${resolvedPath}`);
287
+ }
288
+ }
289
+
290
+ const cmd = `scp ${this.identityArg} ${this.portArg} -o StrictHostKeyChecking=accept-new ${localPath} ${this.destination}:${resolvedPath}`;
291
+
292
+ await this.execWithPassword(cmd);
293
+ console.log(`✓ Upload: ${localPath} → ${resolvedPath}`);
294
+ }
295
+
296
+ async uploadMultiple(
297
+ files: { local: string; remote: string }[],
298
+ options?: UploadOptions
299
+ ): Promise<void> {
300
+ const filteredFiles = files.filter((file) => {
301
+ const fileName = file.local.split("/").pop() || file.local;
302
+ return !this.shouldIgnore(fileName) && !this.shouldIgnore(file.local);
303
+ });
304
+
305
+ const total = filteredFiles.length;
306
+ let uploaded = 0;
307
+
308
+ for (let i = 0; i < total; i++) {
309
+ const file = filteredFiles[i];
310
+ await this.upload(file.local, file.remote, { ...options, overwrite: options?.overwrite });
311
+ uploaded++;
312
+
313
+ if (options?.onProgress) {
314
+ options.onProgress(Math.round((uploaded / total) * 100));
315
+ }
316
+ }
317
+
318
+ console.log(`✓ ${uploaded} fichiers uploadés`);
319
+ }
320
+
321
+ async uploadDirectory(
322
+ localDir: string,
323
+ remoteDir?: string,
324
+ options?: UploadOptions
325
+ ): Promise<void> {
326
+ const targetDir = remoteDir ? this.resolvePath(remoteDir) : this.remoteDir;
327
+
328
+ // Get all files respecting ignorePatterns
329
+ const localFiles = await this.getLocalFiles(localDir);
330
+
331
+ // Create remote directory structure
332
+ const dirs = new Set<string>();
333
+ for (const file of localFiles) {
334
+ const dir = file.split("/").slice(0, -1).join("/");
335
+ if (dir) dirs.add(dir);
336
+ }
337
+
338
+ // Create directories
339
+ for (const dir of Array.from(dirs).sort()) {
340
+ const remoteDirPath = `${targetDir}/${dir}`;
341
+ try {
342
+ await this.mkdir(remoteDirPath, true);
343
+ } catch {
344
+ // Directory might already exist
345
+ }
346
+ }
347
+
348
+ // Upload files
349
+ const total = localFiles.length;
350
+ let uploaded = 0;
351
+
352
+ for (const file of localFiles) {
353
+ const localPath = join(localDir, file);
354
+ const remotePath = `${targetDir}/${file}`;
355
+
356
+ await this.upload(localPath, remotePath, { overwrite: true });
357
+ uploaded++;
358
+
359
+ if (options?.onProgress) {
360
+ options.onProgress(Math.round((uploaded / total) * 100));
361
+ }
362
+ }
363
+
364
+ console.log(`✓ Dossier uploadé: ${localDir} → ${targetDir} (${uploaded} fichiers)`);
365
+ }
366
+
367
+ // --------------------------------------------------------------------------
368
+ // DOWNLOAD
369
+ // --------------------------------------------------------------------------
370
+
371
+ async download(
372
+ remotePath: string,
373
+ localPath: string,
374
+ options?: DownloadOptions
375
+ ): Promise<void> {
376
+ const resolvedPath = this.resolvePath(remotePath);
377
+ const cmd = `scp ${this.identityArg} ${this.portArg} -o StrictHostKeyChecking=accept-new ${this.destination}:${resolvedPath} ${localPath}`;
378
+
379
+ await this.execWithPassword(cmd);
380
+ console.log(`✓ Download: ${resolvedPath} → ${localPath}`);
381
+ }
382
+
383
+ async downloadMultiple(
384
+ files: { remote: string; local: string }[],
385
+ options?: DownloadOptions
386
+ ): Promise<void> {
387
+ const total = files.length;
388
+
389
+ for (let i = 0; i < total; i++) {
390
+ const file = files[i];
391
+ await this.download(file.remote, file.local);
392
+
393
+ if (options?.onProgress) {
394
+ options.onProgress(Math.round(((i + 1) / total) * 100));
395
+ }
396
+ }
397
+
398
+ console.log(`✓ ${total} fichiers téléchargés`);
399
+ }
400
+
401
+ async downloadDirectory(
402
+ remoteDir: string,
403
+ localDir: string,
404
+ options?: DownloadOptions
405
+ ): Promise<void> {
406
+ const resolvedPath = this.resolvePath(remoteDir);
407
+ const cmd = `scp -r ${this.identityArg} ${this.portArg} -o StrictHostKeyChecking=accept-new ${this.destination}:${resolvedPath} ${localDir}`;
408
+
409
+ await this.execWithPassword(cmd);
410
+ console.log(`✓ Dossier téléchargé: ${resolvedPath} → ${localDir}`);
411
+ }
412
+
413
+ // --------------------------------------------------------------------------
414
+ // FILE OPERATIONS
415
+ // --------------------------------------------------------------------------
416
+
417
+ async list(remotePath?: string): Promise<string[]> {
418
+ const resolvedPath = remotePath ? this.resolvePath(remotePath) : this.remoteDir;
419
+ const output = await this.sshExec(`ls -1 "${resolvedPath}"`);
420
+ return this.cleanOutput(output);
421
+ }
422
+
423
+ async listDetailed(remotePath?: string): Promise<FileInfo[]> {
424
+ const resolvedPath = remotePath ? this.resolvePath(remotePath) : this.remoteDir;
425
+ const output = await this.sshExec(
426
+ `ls -la --time-style=+%s "${resolvedPath}" | tail -n +2`
427
+ );
428
+
429
+ const lines = this.cleanOutput(output);
430
+ const files: FileInfo[] = [];
431
+
432
+ for (const line of lines) {
433
+ const parts = line.split(/\s+/);
434
+ if (parts.length >= 7) {
435
+ const permissions = parts[0];
436
+ const size = parseInt(parts[4], 10);
437
+ const timestamp = parseInt(parts[5], 10);
438
+ const name = parts.slice(6).join(" ");
439
+
440
+ if (name && name !== "." && name !== "..") {
441
+ files.push({
442
+ name,
443
+ size,
444
+ isDirectory: permissions.startsWith("d"),
445
+ modified: new Date(timestamp * 1000),
446
+ permissions,
447
+ });
448
+ }
449
+ }
450
+ }
451
+
452
+ return files;
453
+ }
454
+
455
+ async exists(remotePath: string): Promise<boolean> {
456
+ try {
457
+ const resolvedPath = this.resolvePath(remotePath);
458
+ const output = await this.sshExec(
459
+ `test -e "${resolvedPath}" && echo "EXISTS" || echo "NOT_EXISTS"`
460
+ );
461
+ return output.includes("EXISTS");
462
+ } catch {
463
+ return false;
464
+ }
465
+ }
466
+
467
+ async isDirectory(remotePath: string): Promise<boolean> {
468
+ try {
469
+ const resolvedPath = this.resolvePath(remotePath);
470
+ const output = await this.sshExec(
471
+ `test -d "${resolvedPath}" && echo "YES" || echo "NO"`
472
+ );
473
+ return output.includes("YES");
474
+ } catch {
475
+ return false;
476
+ }
477
+ }
478
+
479
+ async isFile(remotePath: string): Promise<boolean> {
480
+ try {
481
+ const resolvedPath = this.resolvePath(remotePath);
482
+ const output = await this.sshExec(
483
+ `test -f "${resolvedPath}" && echo "YES" || echo "NO"`
484
+ );
485
+ return output.includes("YES");
486
+ } catch {
487
+ return false;
488
+ }
489
+ }
490
+
491
+ async stat(remotePath: string): Promise<FileInfo> {
492
+ const resolvedPath = this.resolvePath(remotePath);
493
+ const output = await this.sshExec(
494
+ `stat -c '%n %s %F %Y %A' "${resolvedPath}"`
495
+ );
496
+
497
+ const line = this.cleanOutput(output)[0];
498
+ const parts = line.split(" ");
499
+
500
+ return {
501
+ name: parts[0],
502
+ size: parseInt(parts[1], 10),
503
+ isDirectory: parts[2] === "directory",
504
+ modified: new Date(parseInt(parts[3], 10) * 1000),
505
+ permissions: parts[4],
506
+ };
507
+ }
508
+
509
+ async size(remotePath: string): Promise<number> {
510
+ const resolvedPath = this.resolvePath(remotePath);
511
+ const output = await this.sshExec(`stat -c '%s' "${resolvedPath}"`);
512
+ return parseInt(this.cleanOutput(output)[0], 10);
513
+ }
514
+
515
+ // --------------------------------------------------------------------------
516
+ // DIRECTORY OPERATIONS
517
+ // --------------------------------------------------------------------------
518
+
519
+ async mkdir(remotePath: string, recursive = false): Promise<void> {
520
+ const resolvedPath = this.resolvePath(remotePath);
521
+ const flag = recursive ? "-p" : "";
522
+ await this.sshExec(`mkdir ${flag} "${resolvedPath}"`);
523
+ console.log(`✓ Dossier créé: ${resolvedPath}`);
524
+ }
525
+
526
+ async rmdir(remotePath: string, recursive = false): Promise<void> {
527
+ const resolvedPath = this.resolvePath(remotePath);
528
+ const flag = recursive ? "-rf" : "-d";
529
+ await this.sshExec(`rm ${flag} "${resolvedPath}"`);
530
+ console.log(`✓ Dossier supprimé: ${resolvedPath}`);
531
+ }
532
+
533
+ // --------------------------------------------------------------------------
534
+ // FILE MANIPULATION
535
+ // --------------------------------------------------------------------------
536
+
537
+ async delete(remotePath: string): Promise<void> {
538
+ const resolvedPath = this.resolvePath(remotePath);
539
+ await this.sshExec(`rm -f "${resolvedPath}"`);
540
+ console.log(`✓ Fichier supprimé: ${resolvedPath}`);
541
+ }
542
+
543
+ async rename(oldPath: string, newPath: string): Promise<void> {
544
+ const resolvedOld = this.resolvePath(oldPath);
545
+ const resolvedNew = this.resolvePath(newPath);
546
+ await this.sshExec(`mv "${resolvedOld}" "${resolvedNew}"`);
547
+ console.log(`✓ Renommé: ${resolvedOld} → ${resolvedNew}`);
548
+ }
549
+
550
+ async copy(srcPath: string, destPath: string): Promise<void> {
551
+ const resolvedSrc = this.resolvePath(srcPath);
552
+ const resolvedDest = this.resolvePath(destPath);
553
+ await this.sshExec(`cp "${resolvedSrc}" "${resolvedDest}"`);
554
+ console.log(`✓ Copié: ${resolvedSrc} → ${resolvedDest}`);
555
+ }
556
+
557
+ async chmod(remotePath: string, mode: string | number): Promise<void> {
558
+ const resolvedPath = this.resolvePath(remotePath);
559
+ await this.sshExec(`chmod ${mode} "${resolvedPath}"`);
560
+ console.log(`✓ Permissions: ${resolvedPath} → ${mode}`);
561
+ }
562
+
563
+ async chown(remotePath: string, owner: string, group?: string): Promise<void> {
564
+ const resolvedPath = this.resolvePath(remotePath);
565
+ const ownerGroup = group ? `${owner}:${group}` : owner;
566
+ await this.sshExec(`chown ${ownerGroup} "${resolvedPath}"`);
567
+ console.log(`✓ Propriétaire: ${resolvedPath} → ${ownerGroup}`);
568
+ }
569
+
570
+ // --------------------------------------------------------------------------
571
+ // CONTENT OPERATIONS
572
+ // --------------------------------------------------------------------------
573
+
574
+ async readFile(remotePath: string): Promise<string> {
575
+ const resolvedPath = this.resolvePath(remotePath);
576
+ return await this.sshExec(`cat "${resolvedPath}"`);
577
+ }
578
+
579
+ async writeFile(remotePath: string, content: string): Promise<void> {
580
+ const resolvedPath = this.resolvePath(remotePath);
581
+ const tempFile = join(tmpdir(), `write-${Date.now()}`);
582
+ await Bun.write(tempFile, content);
583
+
584
+ try {
585
+ const cmd = `scp ${this.identityArg} ${this.portArg} -o StrictHostKeyChecking=accept-new ${tempFile} ${this.destination}:${resolvedPath}`;
586
+ await this.execWithPassword(cmd);
587
+ console.log(`✓ Fichier écrit: ${resolvedPath}`);
588
+ } finally {
589
+ await $`rm -f ${tempFile}`.quiet().nothrow();
590
+ }
591
+ }
592
+
593
+ async appendFile(remotePath: string, content: string): Promise<void> {
594
+ const resolvedPath = this.resolvePath(remotePath);
595
+ const escaped = content.replace(/'/g, "'\\''");
596
+ await this.sshExec(`echo '${escaped}' >> "${resolvedPath}"`);
597
+ }
598
+
599
+ // --------------------------------------------------------------------------
600
+ // UTILITY
601
+ // --------------------------------------------------------------------------
602
+
603
+ async exec(command: string): Promise<string> {
604
+ return await this.sshExec(command);
605
+ }
606
+
607
+ async df(remotePath?: string): Promise<{ total: number; used: number; available: number }> {
608
+ const resolvedPath = remotePath ? this.resolvePath(remotePath) : this.remoteDir;
609
+ const output = await this.sshExec(`df -B1 "${resolvedPath}" | tail -1`);
610
+ const parts = this.cleanOutput(output)[0].split(/\s+/);
611
+
612
+ return {
613
+ total: parseInt(parts[1], 10),
614
+ used: parseInt(parts[2], 10),
615
+ available: parseInt(parts[3], 10),
616
+ };
617
+ }
618
+
619
+ async whoami(): Promise<string> {
620
+ const output = await this.sshExec("whoami");
621
+ return this.cleanOutput(output)[0];
622
+ }
623
+
624
+ async pwd(): Promise<string> {
625
+ const output = await this.sshExec("pwd");
626
+ return this.cleanOutput(output)[0];
627
+ }
628
+
629
+ // --------------------------------------------------------------------------
630
+ // GETTERS & SETTERS
631
+ // --------------------------------------------------------------------------
632
+
633
+ getRemoteDir(): string {
634
+ return this.remoteDir;
635
+ }
636
+
637
+ setRemoteDir(dir: string): void {
638
+ this.remoteDir = dir;
639
+ }
640
+
641
+ getIgnorePatterns(): string[] {
642
+ return [...this.ignorePatterns];
643
+ }
644
+
645
+ setIgnorePatterns(patterns: string[]): void {
646
+ this.ignorePatterns = patterns;
647
+ }
648
+
649
+ addIgnorePattern(pattern: string): void {
650
+ if (!this.ignorePatterns.includes(pattern)) {
651
+ this.ignorePatterns.push(pattern);
652
+ }
653
+ }
654
+
655
+ removeIgnorePattern(pattern: string): void {
656
+ this.ignorePatterns = this.ignorePatterns.filter((p) => p !== pattern);
657
+ }
658
+ }
659
+
660
+ // ============================================================================
661
+ // EXPORTS
662
+ // ============================================================================
115
663
 
116
- export { FilesystemSftpAdapter };
664
+ export {
665
+ FilesystemSftpAdapter,
666
+ type SftpConfig,
667
+ type FileInfo,
668
+ type UploadOptions,
669
+ type DownloadOptions,
670
+ };
@@ -0,0 +1,120 @@
1
+ import { Client } from "ssh2";
2
+ import type { SFTPWrapper } from "ssh2";
3
+ import { consola } from "consola";
4
+ import path from "path";
5
+ import chokidar from "chokidar";
6
+ import fs from "fs-extra";
7
+ import type { Collection } from "../../types";
8
+ const BASE_DIR = "/uploads/";
9
+ type ConfigType = {
10
+ host: string;
11
+ port?: number;
12
+ username: string;
13
+ password: string;
14
+ remoteDir: string;
15
+ path?: string;
16
+ ignorePatterns?: string[];
17
+ //privateKeyPath?: string;
18
+ };
19
+ class FilesystemSftpAdapter {
20
+ conn: Client;
21
+ type: "sftp";
22
+ config: ConfigType;
23
+ constructor(
24
+ config = {
25
+ port: 22,
26
+ } as ConfigType
27
+ ) {
28
+ this.config = config;
29
+ this.type = "sftp";
30
+ this.conn = new Client();
31
+
32
+ this.connect()
33
+ .then((e) => {
34
+ e.conn.end();
35
+ })
36
+ .catch((err) => {
37
+ consola.error(`SFTP: Failed to connect ${this.config.host}`.red);
38
+ });
39
+ // this.conn.
40
+ return this;
41
+ }
42
+
43
+
44
+
45
+
46
+
47
+ async syncCollectionMedia(col: Collection) {
48
+ if (!this.config?.remoteDir) this.config.remoteDir = "/home/" + col?.slug;
49
+
50
+ let visibility = col?.media?.visibility || "public";
51
+ let dirToWatch = path.join(process.cwd(), BASE_DIR, col?.slug, visibility);
52
+ dirToWatch = path.resolve(dirToWatch);
53
+ const watcher = chokidar.watch(dirToWatch, {
54
+ persistent: true,
55
+ ignoreInitial: true,
56
+ ignored: this.config.ignorePatterns || [],
57
+ });
58
+
59
+ watcher.on("all", (event, filePath) => {
60
+ if (event == "change" || event == "add") {
61
+ //consola.info(`SFTP: ${event} ${filePath}`);
62
+ let remotePath = path.join(
63
+ this.config.remoteDir,
64
+ path.basename(filePath)
65
+ );
66
+ this.put(filePath, remotePath);
67
+ }
68
+ });
69
+ }
70
+
71
+ connect(): Promise<{ sftp: SFTPWrapper; conn: Client }> {
72
+ return new Promise((resolve, reject) => {
73
+ this.conn
74
+ .on("ready", () => {
75
+ this.conn.sftp((err, sftp) => {
76
+ if (err) {
77
+ this.conn.end();
78
+ return reject(err?.message);
79
+ }
80
+ resolve({
81
+ sftp: sftp,
82
+ conn: this.conn,
83
+ });
84
+ });
85
+ })
86
+ .on("error", (err) => {
87
+ this.conn.end();
88
+ reject(err?.message)
89
+ })
90
+ .connect(this.config);
91
+ });
92
+ }
93
+ async put(localPath: string, remotePath: string) {
94
+ let { sftp, conn } = await this.connect();
95
+ return new Promise(async (resolve, reject) => {
96
+ sftp.fastPut(
97
+ localPath,
98
+ remotePath,
99
+ {
100
+
101
+ mode: 0o777,
102
+ // concurrency
103
+ },
104
+ (err: any) => {
105
+ conn.end();
106
+ if (err) {
107
+ console.error(err?.message || err);
108
+ }
109
+ if (err) {
110
+ reject(err?.message);
111
+ }
112
+
113
+ resolve(true);
114
+ }
115
+ );
116
+ });
117
+ }
118
+ }
119
+
120
+ export { FilesystemSftpAdapter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/core",
3
- "version": "0.77.9-alpha-1",
3
+ "version": "0.78.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "bin": {},
@@ -15,7 +15,7 @@
15
15
  "@types/uuid": "^11.0.0"
16
16
  },
17
17
  "peerDependencies": {
18
- "typescript": "^5.9.2"
18
+ "typescript": "^5.9.3"
19
19
  },
20
20
  "dependencies": {
21
21
  "@colors/colors": "^1.6.0",
@@ -33,7 +33,7 @@
33
33
  "dot-object": "2.1.5",
34
34
  "fs-extra": "^11.2.0",
35
35
  "generate-unique-id": "^2.0.3",
36
- "hono": "4.10.7",
36
+ "hono": "4.11.7",
37
37
  "joi": "18.0.2",
38
38
  "json-joy": "16.8.0",
39
39
  "jsonwebtoken": "^9.0.3",
@@ -48,7 +48,6 @@
48
48
  "radash": "^12.1.0",
49
49
  "rfc6902": "^5.1.2",
50
50
  "sharp": "^0.34.5",
51
- "ssh2": "^1.17.0",
52
51
  "ufo": "^1.5.4",
53
52
  "urlencode": "^2.0.0",
54
53
  "uuid": "^13.0.0"