@simplysm/storage 13.0.100 → 14.0.4

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,32 +1,35 @@
1
1
  import type { Bytes } from "@simplysm/core-common";
2
2
  import { SdError } from "@simplysm/core-common";
3
3
  import SftpClient from "ssh2-sftp-client";
4
+ import fsP from "fs/promises";
5
+ import os from "os";
6
+ import pathMod from "path";
4
7
  import type { StorageClient, FileInfo } from "../types/storage";
5
8
  import type { StorageConnConfig } from "../types/storage-conn-config";
6
9
 
7
- // Buffer usage from ssh2-sftp-client library type definitions
10
+ // ssh2-sftp-client 라이브러리 타입 정의에서의 Buffer 사용
8
11
  type SftpGetResult = string | NodeJS.WritableStream | Bytes;
9
12
 
10
13
  /**
11
- * Storage client using SFTP protocol.
14
+ * SFTP 프로토콜을 사용하는 스토리지 클라이언트.
12
15
  *
13
16
  * @remarks
14
- * Using {@link StorageFactory.connect} is recommended over direct usage.
17
+ * 직접 사용하기보다 {@link StorageFactory.connect} 사용을 권장합니다.
15
18
  */
16
19
  export class SftpStorageClient implements StorageClient {
17
20
  private _client: SftpClient | undefined;
18
21
 
19
22
  /**
20
- * Connect to the SFTP server.
23
+ * SFTP 서버에 연결합니다.
21
24
  *
22
25
  * @remarks
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
+ * - 사용 {@link close} 연결을 종료해야 합니다.
27
+ * - 동일 인스턴스에서 여러 호출하지 마세요 (연결 누수).
28
+ * - 자동 연결/종료 관리를 위해 {@link StorageFactory.connect} 사용을 권장합니다.
26
29
  */
27
30
  async connect(config: StorageConnConfig): Promise<void> {
28
31
  if (this._client !== undefined) {
29
- throw new SdError("SFTP server is already connected. Please call close() first.");
32
+ throw new SdError("SFTP 서버에 이미 연결되어 있습니다. 먼저 close() 호출해 주세요.");
30
33
  }
31
34
 
32
35
  const client = new SftpClient();
@@ -39,10 +42,7 @@ export class SftpStorageClient implements StorageClient {
39
42
  password: config.password,
40
43
  });
41
44
  } else {
42
- // Authenticate with SSH agent + key file
43
- const fsP = await import("fs/promises");
44
- const os = await import("os");
45
- const pathMod = await import("path");
45
+ // SSH agent + 파일로 인증
46
46
  const keyPath = pathMod.join(os.homedir(), ".ssh", "id_ed25519");
47
47
 
48
48
  const baseOptions = {
@@ -58,7 +58,7 @@ export class SftpStorageClient implements StorageClient {
58
58
  privateKey: await fsP.readFile(keyPath),
59
59
  });
60
60
  } catch {
61
- // privateKey parsing failed (encrypted key, etc.) -> retry with agent only
61
+ // privateKey 파싱 실패 (암호화된 ) -> agent만으로 재시도
62
62
  await client.connect(baseOptions);
63
63
  }
64
64
  }
@@ -71,12 +71,12 @@ export class SftpStorageClient implements StorageClient {
71
71
 
72
72
  private _requireClient(): SftpClient {
73
73
  if (this._client === undefined) {
74
- throw new SdError("Not connected to SFTP server.");
74
+ throw new SdError("SFTP 서버에 연결되어 있지 않습니다.");
75
75
  }
76
76
  return this._client;
77
77
  }
78
78
 
79
- /** Create a directory. Creates parent directories if they do not exist. */
79
+ /** 디렉토리를 생성합니다. 부모 디렉토리가 없으면 함께 생성합니다. */
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 StorageClient {
86
86
  }
87
87
 
88
88
  /**
89
- * Check whether a file or directory exists.
89
+ * 파일 또는 디렉토리의 존재 여부를 확인합니다.
90
90
  *
91
91
  * @remarks
92
- * Returns false even if the parent directory does not exist.
93
- * Returns false for all exceptions including network errors and permission errors.
92
+ * 부모 디렉토리가 존재하지 않아도 false를 반환합니다.
93
+ * 네트워크 오류, 권한 오류 모든 예외에 대해 false를 반환합니다.
94
94
  */
95
95
  async exists(filePath: string): Promise<boolean> {
96
96
  try {
97
- // ssh2-sftp-client's exists() returns false | 'd' | '-' | 'l'.
98
- // false: does not exist, 'd': directory, '-': file, 'l': symbolic link
97
+ // ssh2-sftp-client exists() false | 'd' | '-' | 'l'을 반환합니다.
98
+ // false: 존재하지 않음, 'd': 디렉토리, '-': 파일, 'l': 심볼릭 링크
99
99
  const result = await this._requireClient().exists(filePath);
100
100
  return typeof result === "string";
101
101
  } catch {
@@ -112,24 +112,24 @@ export class SftpStorageClient implements StorageClient {
112
112
  }
113
113
 
114
114
  async readFile(filePath: string): Promise<Bytes> {
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.
115
+ // ssh2-sftp-client get() dst가 제공되지 않으면 Buffer를 반환합니다.
116
+ // 타입 정의(string | WritableStream | Buffer) 달리 실제로는 Buffer 반환됩니다.
117
117
  const result = (await this._requireClient().get(filePath)) as SftpGetResult;
118
118
  if (result instanceof Uint8Array) {
119
119
  return result;
120
120
  }
121
- // Defensive code since string is possible per type definition
121
+ // 타입 정의상 string 가능하므로 방어 코드
122
122
  if (typeof result === "string") {
123
123
  return new TextEncoder().encode(result);
124
124
  }
125
- throw new SdError("Unexpected response type.");
125
+ throw new SdError("예상하지 못한 응답 타입입니다.");
126
126
  }
127
127
 
128
128
  async remove(filePath: string): Promise<void> {
129
129
  await this._requireClient().delete(filePath);
130
130
  }
131
131
 
132
- /** Upload a local file path or byte data to the remote path. */
132
+ /** 로컬 파일 경로 또는 바이트 데이터를 원격 경로에 업로드합니다. */
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);
@@ -144,11 +144,11 @@ export class SftpStorageClient implements StorageClient {
144
144
  }
145
145
 
146
146
  /**
147
- * Close the connection.
147
+ * 연결을 종료합니다.
148
148
  *
149
149
  * @remarks
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.
150
+ * 이미 종료된 상태에서 호출해도 안전합니다 (오류 미발생).
151
+ * 종료 동일 인스턴스에서 {@link connect} 다시 호출하여 재연결할 있습니다.
152
152
  */
153
153
  async close(): Promise<void> {
154
154
  if (this._client === undefined) {
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
- // Types
1
+ // 타입
2
2
  export * from "./types/storage-conn-config";
3
3
  export * from "./types/storage";
4
4
  export * from "./types/storage-type";
5
5
 
6
- // Clients
6
+ // 클라이언트
7
7
  export * from "./clients/ftp-storage-client";
8
8
  export * from "./clients/sftp-storage-client";
9
9
 
10
- // Factory
10
+ // 팩토리
11
11
  export * from "./storage-factory";
@@ -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
- * Storage client factory
8
+ * 스토리지 클라이언트 팩토리
9
9
  *
10
- * Creates and manages FTP, FTPS, and SFTP storage connections.
10
+ * FTP, FTPS, SFTP 스토리지 연결을 생성하고 관리합니다.
11
11
  */
12
12
  export class StorageFactory {
13
13
  /**
14
- * Connect to storage, execute the callback, and automatically close the connection.
14
+ * 스토리지에 연결하고, 콜백을 실행한 후, 자동으로 연결을 종료합니다.
15
15
  *
16
16
  * @remarks
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.
17
+ * 콜백 패턴으로 연결/종료를 자동 관리하므로 직접 클라이언트를 사용하는 것보다 권장됩니다.
18
+ * 콜백에서 예외가 발생해도 연결은 자동으로 종료됩니다.
19
19
  */
20
20
  static async connect<R>(
21
21
  type: StorageProtocol,
@@ -29,7 +29,7 @@ export class StorageFactory {
29
29
  return await fn(client);
30
30
  } finally {
31
31
  await client.close().catch(() => {
32
- // Ignore if already closed
32
+ // 이미 종료된 경우 무시
33
33
  });
34
34
  }
35
35
  }
@@ -1,261 +0,0 @@
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
- password: "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 list is called before connection", async () => {
100
- await expect(client.list("/")).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("list", () => {
123
- it("Should return directory list as FileInfo array", async () => {
124
- await client.connect({ host: "test" });
125
- const result = await client.list("/");
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
- });