@simplysm/storage 13.0.69 → 13.0.70

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/clients/sftp-storage-client.ts"],
4
- "mappings": "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",
4
+ "mappings": "AACA,SAAS,eAAe;AACxB,OAAO,gBAAgB;AAahB,MAAM,kBAAqC;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUR,MAAM,QAAQ,QAA0C;AACtD,QAAI,KAAK,YAAY,QAAW;AAC9B,YAAM,IAAI,QAAQ,8DAA8D;AAAA,IAClF;AAEA,UAAM,SAAS,IAAI,WAAW;AAC9B,QAAI;AACF,UAAI,OAAO,QAAQ,MAAM;AACvB,cAAM,OAAO,QAAQ;AAAA,UACnB,MAAM,OAAO;AAAA,UACb,MAAM,OAAO;AAAA,UACb,UAAU,OAAO;AAAA,UACjB,UAAU,OAAO;AAAA,QACnB,CAAC;AAAA,MACH,OAAO;AAEL,cAAM,MAAM,MAAM,OAAO,aAAa;AACtC,cAAM,KAAK,MAAM,OAAO,IAAI;AAC5B,cAAM,UAAU,MAAM,OAAO,MAAM;AACnC,cAAM,UAAU,QAAQ,KAAK,GAAG,QAAQ,GAAG,QAAQ,YAAY;AAE/D,cAAM,cAAc;AAAA,UAClB,MAAM,OAAO;AAAA,UACb,MAAM,OAAO;AAAA,UACb,UAAU,OAAO;AAAA,UACjB,GAAI,QAAQ,IAAI,eAAe,KAAK,OAAO,EAAE,OAAO,QAAQ,IAAI,eAAe,EAAE,IAAI,CAAC;AAAA,QACxF;AAEA,YAAI;AACF,gBAAM,OAAO,QAAQ;AAAA,YACnB,GAAG;AAAA,YACH,YAAY,MAAM,IAAI,SAAS,OAAO;AAAA,UACxC,CAAC;AAAA,QACH,QAAQ;AAEN,gBAAM,OAAO,QAAQ,WAAW;AAAA,QAClC;AAAA,MACF;AACA,WAAK,UAAU;AAAA,IACjB,SAAS,KAAK;AACZ,YAAM,OAAO,IAAI;AACjB,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,iBAA6B;AACnC,QAAI,KAAK,YAAY,QAAW;AAC9B,YAAM,IAAI,QAAQ,+BAA+B;AAAA,IACnD;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,MAAM,SAAgC;AAC1C,UAAM,KAAK,eAAe,EAAE,MAAM,SAAS,IAAI;AAAA,EACjD;AAAA,EAEA,MAAM,OAAO,UAAkB,QAA+B;AAC5D,UAAM,KAAK,eAAe,EAAE,OAAO,UAAU,MAAM;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAO,UAAoC;AAC/C,QAAI;AAGF,YAAM,SAAS,MAAM,KAAK,eAAe,EAAE,OAAO,QAAQ;AAC1D,aAAO,OAAO,WAAW;AAAA,IAC3B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,SAAsC;AAClD,UAAM,OAAO,MAAM,KAAK,eAAe,EAAE,KAAK,OAAO;AACrD,WAAO,KAAK,IAAI,CAAC,UAAU;AAAA,MACzB,MAAM,KAAK;AAAA,MACX,QAAQ,KAAK,SAAS;AAAA,IACxB,EAAE;AAAA,EACJ;AAAA,EAEA,MAAM,SAAS,UAAkC;AAG/C,UAAM,SAAU,MAAM,KAAK,eAAe,EAAE,IAAI,QAAQ;AACxD,QAAI,kBAAkB,YAAY;AAChC,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,WAAW,UAAU;AAC9B,aAAO,IAAI,YAAY,EAAE,OAAO,MAAM;AAAA,IACxC;AACA,UAAM,IAAI,QAAQ,2BAA2B;AAAA,EAC/C;AAAA,EAEA,MAAM,OAAO,UAAiC;AAC5C,UAAM,KAAK,eAAe,EAAE,OAAO,QAAQ;AAAA,EAC7C;AAAA;AAAA,EAGA,MAAM,IAAI,mBAAmC,iBAAwC;AACnF,QAAI,OAAO,sBAAsB,UAAU;AACzC,YAAM,KAAK,eAAe,EAAE,QAAQ,mBAAmB,eAAe;AAAA,IACxE,OAAO;AAEL,YAAM,KAAK,eAAe,EAAE,IAAI,OAAO,KAAK,iBAAiB,GAAG,eAAe;AAAA,IACjF;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,UAAkB,QAA+B;AAC/D,UAAM,KAAK,eAAe,EAAE,UAAU,UAAU,MAAM;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,QAAuB;AAC3B,QAAI,KAAK,YAAY,QAAW;AAC9B;AAAA,IACF;AACA,UAAM,KAAK,QAAQ,IAAI;AACvB,SAAK,UAAU;AAAA,EACjB;AACF;",
5
5
  "names": []
6
6
  }
@@ -2,17 +2,17 @@ import type { StorageConnConfig } from "./types/storage-conn-config";
2
2
  import type { Storage } from "./types/storage";
3
3
  import type { StorageType } from "./types/storage-type";
4
4
  /**
5
- * 스토리지 클라이언트 팩토리
5
+ * Storage client factory
6
6
  *
7
- * FTP, FTPS, SFTP 스토리지 연결을 생성하고 관리한다.
7
+ * Creates and manages FTP, FTPS, and SFTP storage connections.
8
8
  */
9
9
  export declare class StorageFactory {
10
10
  /**
11
- * 스토리지에 연결하고 콜백을 실행한 자동으로 연결을 종료한다.
11
+ * Connect to storage, execute the callback, and automatically close the connection.
12
12
  *
13
13
  * @remarks
14
- * 콜백 패턴으로 연결/종료가 자동 관리되므로, 직접 클라이언트를 사용하는 것보다 권장된다.
15
- * 콜백에서 예외가 발생해도 연결은 자동으로 종료된다.
14
+ * The callback pattern auto-manages connection/close, so this is preferred over direct client usage.
15
+ * The connection is automatically closed even if the callback throws an exception.
16
16
  */
17
17
  static connect<R>(type: StorageType, config: StorageConnConfig, fn: (storage: Storage) => R | Promise<R>): Promise<R>;
18
18
  private static _createClient;
@@ -2,11 +2,11 @@ import { FtpStorageClient } from "./clients/ftp-storage-client.js";
2
2
  import { SftpStorageClient } from "./clients/sftp-storage-client.js";
3
3
  class StorageFactory {
4
4
  /**
5
- * 스토리지에 연결하고 콜백을 실행한 자동으로 연결을 종료한다.
5
+ * Connect to storage, execute the callback, and automatically close the connection.
6
6
  *
7
7
  * @remarks
8
- * 콜백 패턴으로 연결/종료가 자동 관리되므로, 직접 클라이언트를 사용하는 것보다 권장된다.
9
- * 콜백에서 예외가 발생해도 연결은 자동으로 종료된다.
8
+ * The callback pattern auto-manages connection/close, so this is preferred over direct client usage.
9
+ * The connection is automatically closed even if the callback throws an exception.
10
10
  */
11
11
  static async connect(type, config, fn) {
12
12
  const client = StorageFactory._createClient(type);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@simplysm/storage",
3
- "version": "13.0.69",
4
- "description": "심플리즘 패키지 - 스토리지 모듈 (node)",
5
- "author": "김석래",
3
+ "version": "13.0.70",
4
+ "description": "Simplysm Package - Storage Module (node)",
5
+ "author": "simplysm",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
8
8
  "type": "git",
@@ -14,13 +14,14 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "files": [
16
16
  "dist",
17
- "src"
17
+ "src",
18
+ "tests"
18
19
  ],
19
20
  "sideEffects": false,
20
21
  "dependencies": {
21
22
  "basic-ftp": "^5.2.0",
22
23
  "ssh2-sftp-client": "^12.0.1",
23
- "@simplysm/core-common": "13.0.69"
24
+ "@simplysm/core-common": "13.0.70"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/ssh2-sftp-client": "^9.0.6"
@@ -6,11 +6,11 @@ import type { Storage, FileInfo } from "../types/storage";
6
6
  import type { StorageConnConfig } from "../types/storage-conn-config";
7
7
 
8
8
  /**
9
- * FTP/FTPS 프로토콜을 사용하는 스토리지 클라이언트.
9
+ * Storage client using FTP/FTPS protocol.
10
10
  *
11
11
  * @remarks
12
- * 생성자의 `secure` 파라미터로 FTPS 사용 여부를 설정합니다.
13
- * 직접 사용보다 {@link StorageFactory.connect} 통한 사용을 권장합니다.
12
+ * The `secure` constructor parameter configures whether to use FTPS.
13
+ * Using {@link StorageFactory.connect} is recommended over direct usage.
14
14
  */
15
15
  export class FtpStorageClient implements Storage {
16
16
  private _client: ftp.Client | undefined;
@@ -18,16 +18,16 @@ export class FtpStorageClient implements Storage {
18
18
  constructor(private readonly _secure: boolean = false) {}
19
19
 
20
20
  /**
21
- * FTP 서버에 연결합니다.
21
+ * Connect to the FTP server.
22
22
  *
23
23
  * @remarks
24
- * - 연결 반드시 {@link close} 연결을 종료해야 합니다.
25
- * - 동일 인스턴스에서 여러 호출하지 마세요. (연결 누수 발생)
26
- * - 자동 연결/종료 관리가 필요하면 {@link StorageFactory.connect} 사용하세요. (권장)
24
+ * - Must close the connection with {@link close} after use.
25
+ * - Do not call multiple times on the same instance (connection leak).
26
+ * - Use {@link StorageFactory.connect} for automatic connection/close management (recommended).
27
27
  */
28
28
  async connect(config: StorageConnConfig): Promise<void> {
29
29
  if (this._client !== undefined) {
30
- throw new SdError("이미 FTP 서버에 연결되어 있습니다. 먼저 close() 호출하세요.");
30
+ throw new SdError("FTP server is already connected. Please call close() first.");
31
31
  }
32
32
  const client = new ftp.Client();
33
33
  try {
@@ -47,12 +47,12 @@ export class FtpStorageClient implements Storage {
47
47
 
48
48
  private _requireClient(): ftp.Client {
49
49
  if (this._client === undefined) {
50
- throw new SdError("FTP 서버에 연결되어있지 않습니다.");
50
+ throw new SdError("Not connected to FTP server.");
51
51
  }
52
52
  return this._client;
53
53
  }
54
54
 
55
- /** 디렉토리를 생성합니다. 상위 디렉토리가 없으면 함께 생성합니다. */
55
+ /** Create a directory. Creates parent directories if they do not exist. */
56
56
  async mkdir(dirPath: string): Promise<void> {
57
57
  await this._requireClient().ensureDir(dirPath);
58
58
  }
@@ -78,24 +78,24 @@ export class FtpStorageClient implements Storage {
78
78
  }
79
79
 
80
80
  /**
81
- * 파일 또는 디렉토리 존재 여부를 확인합니다.
81
+ * Check whether a file or directory exists.
82
82
  *
83
83
  * @remarks
84
- * 파일 확인 size() 명령으로 O(1) 성능을 제공합니다.
85
- * 디렉토리 확인 상위 디렉토리 목록을 조회하므로, 항목 수가 많으면 성능이 저하될 있습니다.
84
+ * For files, uses the size() command for O(1) performance.
85
+ * For directories, queries the parent directory listing, so performance may degrade with many entries.
86
86
  *
87
- * 슬래시가 없는 경로(예: `file.txt`) 루트 디렉토리(`/`)에서 검색합니다.
87
+ * Paths without slashes (e.g. `file.txt`) are searched in the root directory (`/`).
88
88
  *
89
- * 상위 디렉토리가 존재하지 않는 경우에도 false를 반환합니다.
90
- * 네트워크 오류, 권한 오류 모든 예외도 false를 반환합니다.
89
+ * Returns false even if the parent directory does not exist.
90
+ * Returns false for all exceptions including network errors and permission errors.
91
91
  */
92
92
  async exists(filePath: string): Promise<boolean> {
93
93
  try {
94
- // 파일인 경우 size() 빠르게 확인 (O(1))
94
+ // Quick check for files via size() (O(1))
95
95
  await this._requireClient().size(filePath);
96
96
  return true;
97
97
  } catch {
98
- // size() 실패 디렉토리일 있으므로 list()로 확인
98
+ // If size() fails, it may be a directory, so check via list()
99
99
  try {
100
100
  const lastSlash = filePath.lastIndexOf("/");
101
101
  const dirPath = lastSlash > 0 ? filePath.substring(0, lastSlash) : "/";
@@ -112,7 +112,7 @@ export class FtpStorageClient implements Storage {
112
112
  await this._requireClient().remove(filePath);
113
113
  }
114
114
 
115
- /** 로컬 파일 경로 또는 바이트 데이터를 원격 경로에 업로드합니다. */
115
+ /** Upload a local file path or byte data to the remote path. */
116
116
  async put(localPathOrBuffer: string | Bytes, storageFilePath: string): Promise<void> {
117
117
  let param: string | Readable;
118
118
  if (typeof localPathOrBuffer === "string") {
@@ -128,11 +128,11 @@ export class FtpStorageClient implements Storage {
128
128
  }
129
129
 
130
130
  /**
131
- * 연결을 종료합니다.
131
+ * Close the connection.
132
132
  *
133
133
  * @remarks
134
- * 이미 종료된 상태에서 호출해도 에러가 발생하지 않습니다.
135
- * 종료 후에는 동일 인스턴스에서 {@link connect} 다시 호출하여 재연결할 있습니다.
134
+ * Safe to call when already closed (no error thrown).
135
+ * After closing, you can reconnect by calling {@link connect} again on the same instance.
136
136
  */
137
137
  close(): Promise<void> {
138
138
  if (this._client === undefined) {
@@ -4,29 +4,29 @@ import SftpClient from "ssh2-sftp-client";
4
4
  import type { Storage, FileInfo } from "../types/storage";
5
5
  import type { StorageConnConfig } from "../types/storage-conn-config";
6
6
 
7
- // ssh2-sftp-client 라이브러리 타입 정의에서 Buffer 사용
7
+ // Buffer usage from ssh2-sftp-client library type definitions
8
8
  type SftpGetResult = string | NodeJS.WritableStream | Bytes;
9
9
 
10
10
  /**
11
- * SFTP 프로토콜을 사용하는 스토리지 클라이언트.
11
+ * Storage client using SFTP protocol.
12
12
  *
13
13
  * @remarks
14
- * 직접 사용보다 {@link StorageFactory.connect} 통한 사용을 권장합니다.
14
+ * Using {@link StorageFactory.connect} is recommended over direct usage.
15
15
  */
16
16
  export class SftpStorageClient implements Storage {
17
17
  private _client: SftpClient | undefined;
18
18
 
19
19
  /**
20
- * SFTP 서버에 연결합니다.
20
+ * Connect to the SFTP server.
21
21
  *
22
22
  * @remarks
23
- * - 연결 반드시 {@link close} 연결을 종료해야 합니다.
24
- * - 동일 인스턴스에서 여러 호출하지 마세요. (연결 누수 발생)
25
- * - 자동 연결/종료 관리가 필요하면 {@link StorageFactory.connect} 사용하세요. (권장)
23
+ * - Must close the connection with {@link close} after use.
24
+ * - Do not call multiple times on the same instance (connection leak).
25
+ * - Use {@link StorageFactory.connect} for automatic connection/close management (recommended).
26
26
  */
27
27
  async connect(config: StorageConnConfig): Promise<void> {
28
28
  if (this._client !== undefined) {
29
- throw new SdError("이미 SFTP 서버에 연결되어 있습니다. 먼저 close() 호출하세요.");
29
+ throw new SdError("SFTP server is already connected. Please call close() first.");
30
30
  }
31
31
 
32
32
  const client = new SftpClient();
@@ -39,7 +39,7 @@ export class SftpStorageClient implements Storage {
39
39
  password: config.pass,
40
40
  });
41
41
  } else {
42
- // SSH agent + 파일로 인증 시도
42
+ // Authenticate with SSH agent + key file
43
43
  const fsP = await import("fs/promises");
44
44
  const os = await import("os");
45
45
  const pathMod = await import("path");
@@ -58,7 +58,7 @@ export class SftpStorageClient implements Storage {
58
58
  privateKey: await fsP.readFile(keyPath),
59
59
  });
60
60
  } catch {
61
- // privateKey 파싱 실패 (암호화된 ) agent만으로 재시도
61
+ // privateKey parsing failed (encrypted key, etc.) -> retry with agent only
62
62
  await client.connect(baseOptions);
63
63
  }
64
64
  }
@@ -71,12 +71,12 @@ export class SftpStorageClient implements Storage {
71
71
 
72
72
  private _requireClient(): SftpClient {
73
73
  if (this._client === undefined) {
74
- throw new SdError("SFTP 서버에 연결되어있지 않습니다.");
74
+ throw new SdError("Not connected to SFTP server.");
75
75
  }
76
76
  return this._client;
77
77
  }
78
78
 
79
- /** 디렉토리를 생성합니다. 상위 디렉토리가 없으면 함께 생성합니다. */
79
+ /** Create a directory. Creates parent directories if they do not exist. */
80
80
  async mkdir(dirPath: string): Promise<void> {
81
81
  await this._requireClient().mkdir(dirPath, true);
82
82
  }
@@ -86,16 +86,16 @@ export class SftpStorageClient implements Storage {
86
86
  }
87
87
 
88
88
  /**
89
- * 파일 또는 디렉토리 존재 여부를 확인합니다.
89
+ * Check whether a file or directory exists.
90
90
  *
91
91
  * @remarks
92
- * 상위 디렉토리가 존재하지 않는 경우에도 false를 반환합니다.
93
- * 네트워크 오류, 권한 오류 모든 예외도 false를 반환합니다.
92
+ * Returns false even if the parent directory does not exist.
93
+ * Returns false for all exceptions including network errors and permission errors.
94
94
  */
95
95
  async exists(filePath: string): Promise<boolean> {
96
96
  try {
97
- // ssh2-sftp-client exists() false | 'd' | '-' | 'l' 를 반환한다.
98
- // false: 존재하지 않음, 'd': 디렉토리, '-': 파일, 'l': 심볼릭 링크
97
+ // ssh2-sftp-client's exists() returns false | 'd' | '-' | 'l'.
98
+ // false: does not exist, 'd': directory, '-': file, 'l': symbolic link
99
99
  const result = await this._requireClient().exists(filePath);
100
100
  return typeof result === "string";
101
101
  } catch {
@@ -112,29 +112,29 @@ export class SftpStorageClient implements Storage {
112
112
  }
113
113
 
114
114
  async readFile(filePath: string): Promise<Bytes> {
115
- // ssh2-sftp-client get() dst 미전달 Buffer를 반환한다.
116
- // 타입 정의(string | WritableStream | Buffer) 달리 실제로는 Buffer 반환된다.
115
+ // ssh2-sftp-client's get() returns Buffer when dst is not provided.
116
+ // Despite the type definition (string | WritableStream | Buffer), only Buffer is actually returned.
117
117
  const result = (await this._requireClient().get(filePath)) as SftpGetResult;
118
118
  if (result instanceof Uint8Array) {
119
119
  return result;
120
120
  }
121
- // 타입 정의상 string 가능하므로 방어 코드
121
+ // Defensive code since string is possible per type definition
122
122
  if (typeof result === "string") {
123
123
  return new TextEncoder().encode(result);
124
124
  }
125
- throw new SdError("예상치 못한 응답 타입입니다.");
125
+ throw new SdError("Unexpected response type.");
126
126
  }
127
127
 
128
128
  async remove(filePath: string): Promise<void> {
129
129
  await this._requireClient().delete(filePath);
130
130
  }
131
131
 
132
- /** 로컬 파일 경로 또는 바이트 데이터를 원격 경로에 업로드합니다. */
132
+ /** Upload a local file path or byte data to the remote path. */
133
133
  async put(localPathOrBuffer: string | Bytes, storageFilePath: string): Promise<void> {
134
134
  if (typeof localPathOrBuffer === "string") {
135
135
  await this._requireClient().fastPut(localPathOrBuffer, storageFilePath);
136
136
  } else {
137
- // eslint-disable-next-line no-restricted-globals -- ssh2-sftp-client 라이브러리 요구사항
137
+ // eslint-disable-next-line no-restricted-globals -- ssh2-sftp-client library requirement
138
138
  await this._requireClient().put(Buffer.from(localPathOrBuffer), storageFilePath);
139
139
  }
140
140
  }
@@ -144,11 +144,11 @@ export class SftpStorageClient implements Storage {
144
144
  }
145
145
 
146
146
  /**
147
- * 연결을 종료합니다.
147
+ * Close the connection.
148
148
  *
149
149
  * @remarks
150
- * 이미 종료된 상태에서 호출해도 에러가 발생하지 않습니다.
151
- * 종료 후에는 동일 인스턴스에서 {@link connect} 다시 호출하여 재연결할 있습니다.
150
+ * Safe to call when already closed (no error thrown).
151
+ * After closing, you can reconnect by calling {@link connect} again on the same instance.
152
152
  */
153
153
  async close(): Promise<void> {
154
154
  if (this._client === undefined) {
@@ -5,17 +5,17 @@ import { FtpStorageClient } from "./clients/ftp-storage-client";
5
5
  import { SftpStorageClient } from "./clients/sftp-storage-client";
6
6
 
7
7
  /**
8
- * 스토리지 클라이언트 팩토리
8
+ * Storage client factory
9
9
  *
10
- * FTP, FTPS, SFTP 스토리지 연결을 생성하고 관리한다.
10
+ * Creates and manages FTP, FTPS, and SFTP storage connections.
11
11
  */
12
12
  export class StorageFactory {
13
13
  /**
14
- * 스토리지에 연결하고 콜백을 실행한 자동으로 연결을 종료한다.
14
+ * Connect to storage, execute the callback, and automatically close the connection.
15
15
  *
16
16
  * @remarks
17
- * 콜백 패턴으로 연결/종료가 자동 관리되므로, 직접 클라이언트를 사용하는 것보다 권장된다.
18
- * 콜백에서 예외가 발생해도 연결은 자동으로 종료된다.
17
+ * The callback pattern auto-manages connection/close, so this is preferred over direct client usage.
18
+ * The connection is automatically closed even if the callback throws an exception.
19
19
  */
20
20
  static async connect<R>(
21
21
  type: StorageType,
@@ -29,7 +29,7 @@ export class StorageFactory {
29
29
  return await fn(client);
30
30
  } finally {
31
31
  await client.close().catch(() => {
32
- // 이미 닫힌 경우 무시
32
+ // Ignore if already closed
33
33
  });
34
34
  }
35
35
  }
@@ -0,0 +1,261 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { FtpStorageClient } from "../src/clients/ftp-storage-client";
3
+
4
+ // Mock basic-ftp module
5
+ const mockAccess = vi.fn().mockResolvedValue(undefined);
6
+ const mockEnsureDir = vi.fn().mockResolvedValue(undefined);
7
+ const mockRename = vi.fn().mockResolvedValue(undefined);
8
+ const mockSize = vi.fn().mockResolvedValue(100);
9
+ const mockList = vi.fn().mockResolvedValue([
10
+ { name: "file.txt", isFile: true },
11
+ { name: "dir", isFile: false },
12
+ ]);
13
+ const mockDownloadTo = vi.fn().mockImplementation((writable) => {
14
+ writable.emit("data", new TextEncoder().encode("test content"));
15
+ return Promise.resolve();
16
+ });
17
+ const mockRemove = vi.fn().mockResolvedValue(undefined);
18
+ const mockUploadFrom = vi.fn().mockResolvedValue(undefined);
19
+ const mockUploadFromDir = vi.fn().mockResolvedValue(undefined);
20
+ const mockClose = vi.fn();
21
+
22
+ vi.mock("basic-ftp", () => {
23
+ return {
24
+ default: {
25
+ Client: class MockClient {
26
+ access = mockAccess;
27
+ ensureDir = mockEnsureDir;
28
+ rename = mockRename;
29
+ size = mockSize;
30
+ list = mockList;
31
+ downloadTo = mockDownloadTo;
32
+ remove = mockRemove;
33
+ uploadFrom = mockUploadFrom;
34
+ uploadFromDir = mockUploadFromDir;
35
+ close = mockClose;
36
+ },
37
+ },
38
+ };
39
+ });
40
+
41
+ describe("FtpStorageClient", () => {
42
+ let client: FtpStorageClient;
43
+
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+ client = new FtpStorageClient();
47
+ });
48
+
49
+ describe("connect", () => {
50
+ it("Should connect with connection settings", async () => {
51
+ await client.connect({
52
+ host: "ftp.example.com",
53
+ port: 21,
54
+ user: "user",
55
+ pass: "pass",
56
+ });
57
+
58
+ expect(mockAccess).toHaveBeenCalledWith({
59
+ host: "ftp.example.com",
60
+ port: 21,
61
+ user: "user",
62
+ password: "pass",
63
+ secure: false,
64
+ });
65
+ });
66
+
67
+ it("Should connect in secure mode", async () => {
68
+ const secureClient = new FtpStorageClient(true);
69
+ await secureClient.connect({ host: "ftp.example.com" });
70
+
71
+ expect(mockAccess).toHaveBeenCalledWith(expect.objectContaining({ secure: true }));
72
+ });
73
+
74
+ it("Should throw error when connect is called on already connected client", async () => {
75
+ await client.connect({ host: "test" });
76
+ await expect(client.connect({ host: "test" })).rejects.toThrow(
77
+ "FTP server is already connected. Please call close() first.",
78
+ );
79
+ });
80
+
81
+ it("Should clean up client on connection failure", async () => {
82
+ mockAccess.mockRejectedValueOnce(new Error("Auth failed"));
83
+ await expect(client.connect({ host: "test" })).rejects.toThrow("Auth failed");
84
+ expect(mockClose).toHaveBeenCalled();
85
+ });
86
+ });
87
+
88
+ describe("Method calls before connection", () => {
89
+ it("Should throw error when mkdir is called before connection", async () => {
90
+ await expect(client.mkdir("/test")).rejects.toThrow("Not connected to FTP server.");
91
+ });
92
+
93
+ it("Should throw error when rename is called before connection", async () => {
94
+ await expect(client.rename("/from", "/to")).rejects.toThrow(
95
+ "Not connected to FTP server.",
96
+ );
97
+ });
98
+
99
+ it("Should throw error when readdir is called before connection", async () => {
100
+ await expect(client.readdir("/")).rejects.toThrow("Not connected to FTP server.");
101
+ });
102
+ });
103
+
104
+ describe("mkdir", () => {
105
+ it("Should create directory", async () => {
106
+ await client.connect({ host: "test" });
107
+ await client.mkdir("/test/dir");
108
+
109
+ expect(mockEnsureDir).toHaveBeenCalledWith("/test/dir");
110
+ });
111
+ });
112
+
113
+ describe("rename", () => {
114
+ it("Should rename file/directory", async () => {
115
+ await client.connect({ host: "test" });
116
+ await client.rename("/from", "/to");
117
+
118
+ expect(mockRename).toHaveBeenCalledWith("/from", "/to");
119
+ });
120
+ });
121
+
122
+ describe("readdir", () => {
123
+ it("Should return directory list as FileInfo array", async () => {
124
+ await client.connect({ host: "test" });
125
+ const result = await client.readdir("/");
126
+
127
+ expect(result).toEqual([
128
+ { name: "file.txt", isFile: true },
129
+ { name: "dir", isFile: false },
130
+ ]);
131
+ });
132
+ });
133
+
134
+ describe("readFile", () => {
135
+ it("Should return file content as Uint8Array", async () => {
136
+ await client.connect({ host: "test" });
137
+ const result = await client.readFile("/file.txt");
138
+
139
+ expect(result).toBeInstanceOf(Uint8Array);
140
+ expect(new TextDecoder().decode(result)).toBe("test content");
141
+ });
142
+ });
143
+
144
+ describe("exists", () => {
145
+ it("Should return true if file exists (checked via size)", async () => {
146
+ await client.connect({ host: "test" });
147
+ const result = await client.exists("/path/file.txt");
148
+
149
+ expect(mockSize).toHaveBeenCalledWith("/path/file.txt");
150
+ expect(result).toBe(true);
151
+ });
152
+
153
+ it("Should return true if directory exists (checked via list)", async () => {
154
+ mockSize.mockRejectedValueOnce(new Error("Not a file"));
155
+ await client.connect({ host: "test" });
156
+ const result = await client.exists("/path/dir");
157
+
158
+ expect(mockList).toHaveBeenCalledWith("/path");
159
+ expect(result).toBe(true);
160
+ });
161
+
162
+ it("Should return false if file does not exist", async () => {
163
+ mockSize.mockRejectedValueOnce(new Error("Not a file"));
164
+ mockList.mockResolvedValueOnce([]);
165
+ await client.connect({ host: "test" });
166
+ const result = await client.exists("/path/nonexistent.txt");
167
+
168
+ expect(result).toBe(false);
169
+ });
170
+
171
+ it("Should return false on error", async () => {
172
+ mockSize.mockRejectedValueOnce(new Error("Not a file"));
173
+ mockList.mockRejectedValueOnce(new Error("Not found"));
174
+ await client.connect({ host: "test" });
175
+ const result = await client.exists("/path/error.txt");
176
+
177
+ expect(result).toBe(false);
178
+ });
179
+
180
+ it("Should check file existence in root directory", async () => {
181
+ mockSize.mockRejectedValueOnce(new Error("Not a file"));
182
+ await client.connect({ host: "test" });
183
+ const result = await client.exists("/file.txt");
184
+
185
+ expect(mockList).toHaveBeenCalledWith("/");
186
+ expect(result).toBe(true);
187
+ });
188
+
189
+ it("Should check file existence for paths without slashes", async () => {
190
+ mockSize.mockRejectedValueOnce(new Error("Not a file"));
191
+ await client.connect({ host: "test" });
192
+ const result = await client.exists("file.txt");
193
+
194
+ expect(mockList).toHaveBeenCalledWith("/");
195
+ expect(result).toBe(true);
196
+ });
197
+ });
198
+
199
+ describe("remove", () => {
200
+ it("Should delete file", async () => {
201
+ await client.connect({ host: "test" });
202
+ await client.remove("/file.txt");
203
+
204
+ expect(mockRemove).toHaveBeenCalledWith("/file.txt");
205
+ });
206
+ });
207
+
208
+ describe("put", () => {
209
+ it("Should upload from local path", async () => {
210
+ await client.connect({ host: "test" });
211
+ await client.put("/local/file.txt", "/remote/file.txt");
212
+
213
+ expect(mockUploadFrom).toHaveBeenCalledWith("/local/file.txt", "/remote/file.txt");
214
+ });
215
+
216
+ it("Should upload from Uint8Array", async () => {
217
+ await client.connect({ host: "test" });
218
+ const bytes = new TextEncoder().encode("content");
219
+ await client.put(bytes, "/remote/file.txt");
220
+
221
+ expect(mockUploadFrom).toHaveBeenCalled();
222
+ });
223
+ });
224
+
225
+ describe("uploadDir", () => {
226
+ it("Should upload directory", async () => {
227
+ await client.connect({ host: "test" });
228
+ await client.uploadDir("/local/dir", "/remote/dir");
229
+
230
+ expect(mockUploadFromDir).toHaveBeenCalledWith("/local/dir", "/remote/dir");
231
+ });
232
+ });
233
+
234
+ describe("close", () => {
235
+ it("Should exit without error when close is called before connection", async () => {
236
+ await expect(client.close()).resolves.toBeUndefined();
237
+ });
238
+
239
+ it("Should close connection", async () => {
240
+ await client.connect({ host: "test" });
241
+ await client.close();
242
+
243
+ expect(mockClose).toHaveBeenCalled();
244
+ });
245
+
246
+ it("Should throw error when calling method after close", async () => {
247
+ await client.connect({ host: "test" });
248
+ await client.close();
249
+
250
+ await expect(client.mkdir("/test")).rejects.toThrow("Not connected to FTP server.");
251
+ });
252
+
253
+ it("Should allow reconnection after close", async () => {
254
+ await client.connect({ host: "test" });
255
+ await client.close();
256
+ await client.connect({ host: "test" });
257
+
258
+ expect(mockAccess).toHaveBeenCalledTimes(2);
259
+ });
260
+ });
261
+ });