@yimingliao/cms 0.0.18 → 0.0.19

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.
@@ -4,6 +4,8 @@ import { cookies } from 'next/headers';
4
4
  import { S3Client } from '@aws-sdk/client-s3';
5
5
  import { Logger } from 'logry';
6
6
  import { i as BlobFile, e as AdminRole, w as SingleItem, B as BaseTranslation, a as Admin, c as AdminFull, f as AdminSafe, D as DeviceInfo, d as AdminRefreshToken, l as File, n as FileFull, p as FileType, q as Folder, F as FolderFull, v as PostType, M as MultiItems, E as ExternalLink, k as Faq, T as TocItem, r as Post, t as PostListCard, u as PostTranslation, s as PostFull, S as SeoMetadata, g as AdminTranslation, o as FileTranslation, h as Alternate } from '../types-Bhnz5Z1F.js';
7
+ import { Pool } from 'generic-pool';
8
+ import SFTPClient from 'ssh2-sftp-client';
7
9
  import Keyv from 'keyv';
8
10
 
9
11
  interface CreateJwtServiceOptions {
@@ -72,24 +74,39 @@ declare function createCookieService(nextCookies: () => Promise<Awaited<ReturnTy
72
74
  }) => Promise<void>;
73
75
  };
74
76
 
75
- interface CreateR2ServiceOptions {
76
- r2Client: S3Client;
77
- bucketName: string;
78
- logger: Logger;
79
- }
80
- declare function createR2Service({ r2Client, bucketName, logger, }: CreateR2ServiceOptions): {
81
- upload: ({ blobFile, folderKey, }: {
77
+ interface StorageService {
78
+ upload(options: {
82
79
  blobFile: BlobFile;
83
80
  folderKey?: string;
84
- }) => Promise<string>;
85
- remove: ({ key }: {
81
+ }): Promise<string>;
82
+ remove(options: {
86
83
  key: string;
87
- }) => Promise<void>;
88
- move: ({ fromKey, toKey }: {
84
+ }): Promise<void>;
85
+ move(options: {
89
86
  fromKey: string;
90
87
  toKey: string;
91
- }) => Promise<void>;
92
- };
88
+ }): Promise<void>;
89
+ }
90
+
91
+ interface CreateR2ServiceOptions {
92
+ r2Client: S3Client;
93
+ bucketName: string;
94
+ logger: Logger;
95
+ }
96
+ declare function createR2Service({ r2Client, bucketName, logger, }: CreateR2ServiceOptions): StorageService;
97
+
98
+ interface CreateSftpServiceOptions {
99
+ sftpPool: Pool<SFTPClient>;
100
+ basePath: string;
101
+ logger: Logger;
102
+ }
103
+ declare function createSftpService({ sftpPool, basePath, logger, }: CreateSftpServiceOptions): StorageService;
104
+
105
+ interface CreateSftpPoolOptions {
106
+ sftpClientOptions: SFTPClient.ConnectOptions;
107
+ logger: Logger;
108
+ }
109
+ declare const createSftpPool: ({ sftpClientOptions, logger, }: CreateSftpPoolOptions) => Pool<SFTPClient>;
93
110
 
94
111
  interface CreateServerCacheOptions {
95
112
  redisUrl: string;
@@ -616,4 +633,4 @@ declare const POST_ORDER_BY: ({
616
633
  index: "asc";
617
634
  })[];
618
635
 
619
- export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, type RawCacheKey, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCache, createCacheResult, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createIpRateLimiter, createJwtService, createPostCommandRepository, createPostQueryRepository, createR2Service, createSeoMetadataCommandRepository, normalizeCacheKey };
636
+ export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, type RawCacheKey, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCache, createCacheResult, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createIpRateLimiter, createJwtService, createPostCommandRepository, createPostQueryRepository, createR2Service, createSeoMetadataCommandRepository, createSftpPool, createSftpService, normalizeCacheKey };
@@ -4,8 +4,10 @@ import argon2 from 'argon2';
4
4
  import crypto, { timingSafeEqual } from 'crypto';
5
5
  import { headers } from 'next/headers';
6
6
  import { PutObjectCommand, DeleteObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
7
- import path from 'path/posix';
7
+ import path2 from 'path/posix';
8
8
  import { ulid } from 'ulid';
9
+ import { createPool } from 'generic-pool';
10
+ import SFTPClient from 'ssh2-sftp-client';
9
11
  import KeyvRedis from '@keyv/redis';
10
12
  import Keyv from 'keyv';
11
13
 
@@ -205,17 +207,17 @@ function createObjectKey({
205
207
  mimeType,
206
208
  folderKey = ""
207
209
  }) {
208
- const extensionFromFilename = path.extname(filename ?? "").toLowerCase();
210
+ const extensionFromFilename = path2.extname(filename ?? "").toLowerCase();
209
211
  const extensionFromMime = !extensionFromFilename && mimeType ? `.${mimeToExtension(mimeType)}` : "";
210
212
  const extension = extensionFromFilename || extensionFromMime;
211
213
  const normalizedFolder = folderKey.replace(/^\/+/, "").replace(/\/+$/, "");
212
214
  const id = ulid();
213
215
  const filenameWithId = id + extension;
214
216
  if (!normalizedFolder) return filenameWithId;
215
- return path.join(normalizedFolder, filenameWithId);
217
+ return path2.join(normalizedFolder, filenameWithId);
216
218
  }
217
219
 
218
- // src/server/infrastructure/r2/r2.service.ts
220
+ // src/server/infrastructure/storage/r2/r2.service.ts
219
221
  function createR2Service({
220
222
  r2Client,
221
223
  bucketName,
@@ -243,7 +245,7 @@ function createR2Service({
243
245
  logger.debug("R2 upload success", { key });
244
246
  return key;
245
247
  } catch (error) {
246
- logger.error("upload failed", { key, error });
248
+ logger.error("R2 upload failed", { key, error });
247
249
  throw error;
248
250
  }
249
251
  }
@@ -257,7 +259,7 @@ function createR2Service({
257
259
  );
258
260
  logger.debug("R2 object removed", { key });
259
261
  } catch (error) {
260
- logger.error("Remove failed", { key, error });
262
+ logger.error("R2 remove failed", { key, error });
261
263
  throw error;
262
264
  }
263
265
  }
@@ -277,7 +279,7 @@ function createR2Service({
277
279
  await remove({ key: fromKey });
278
280
  logger.debug("R2 object moved", { fromKey, toKey });
279
281
  } catch (error) {
280
- logger.error("Move failed", { fromKey, toKey, error });
282
+ logger.error("R2 move failed", { fromKey, toKey, error });
281
283
  throw error;
282
284
  }
283
285
  }
@@ -287,6 +289,123 @@ function createR2Service({
287
289
  move
288
290
  };
289
291
  }
292
+ function createSftpService({
293
+ sftpPool,
294
+ basePath,
295
+ logger
296
+ }) {
297
+ async function upload({
298
+ blobFile,
299
+ folderKey = ""
300
+ }) {
301
+ const key = createObjectKey({
302
+ filename: blobFile.name,
303
+ mimeType: blobFile.type,
304
+ folderKey
305
+ });
306
+ const fullPath = path2.join(basePath, key);
307
+ const client = await sftpPool.acquire();
308
+ try {
309
+ const buffer = Buffer.from(await blobFile.arrayBuffer());
310
+ await client.put(buffer, fullPath);
311
+ logger.debug("SFTP upload success", { key });
312
+ return key;
313
+ } catch (error) {
314
+ logger.error("SFTP upload failed", { key, error });
315
+ throw error;
316
+ } finally {
317
+ await sftpPool.release(client);
318
+ }
319
+ }
320
+ async function remove({ key }) {
321
+ const fullPath = path2.join(basePath, key);
322
+ const client = await sftpPool.acquire();
323
+ try {
324
+ await client.delete(fullPath);
325
+ logger.debug("SFTP object removed", { key });
326
+ } catch (error) {
327
+ if (error instanceof Error && /no such file/i.test(error.message)) {
328
+ logger.debug("SFTP remove skipped (not found)", { key });
329
+ return;
330
+ }
331
+ logger.error("SFTP remove failed", { key, error });
332
+ throw error;
333
+ } finally {
334
+ await sftpPool.release(client);
335
+ }
336
+ }
337
+ async function move({ fromKey, toKey }) {
338
+ if (fromKey === toKey) {
339
+ logger.debug("SFTP move skipped (same key)", { key: fromKey });
340
+ return;
341
+ }
342
+ const fromPath = path2.join(basePath, fromKey);
343
+ const toPath = path2.join(basePath, toKey);
344
+ const client = await sftpPool.acquire();
345
+ try {
346
+ await client.rename(fromPath, toPath);
347
+ logger.debug("SFTP object moved", { fromKey, toKey });
348
+ } catch (error) {
349
+ logger.error("SFTP move failed", { fromKey, toKey, error });
350
+ throw error;
351
+ } finally {
352
+ await sftpPool.release(client);
353
+ }
354
+ }
355
+ return {
356
+ upload,
357
+ remove,
358
+ move
359
+ };
360
+ }
361
+ var createSftpPool = ({
362
+ sftpClientOptions,
363
+ logger
364
+ }) => {
365
+ return createPool(
366
+ {
367
+ create: async () => {
368
+ const client = new SFTPClient();
369
+ try {
370
+ await client.connect({
371
+ readyTimeout: 2e4,
372
+ keepaliveInterval: 1e4,
373
+ keepaliveCountMax: 3,
374
+ ...sftpClientOptions
375
+ });
376
+ return client;
377
+ } catch (error) {
378
+ logger.error("SFTP connect failed", { error });
379
+ throw error;
380
+ }
381
+ },
382
+ validate: async (client) => {
383
+ try {
384
+ await client.realPath("/");
385
+ return true;
386
+ } catch {
387
+ logger.debug("SFTP session invalid, removing from pool");
388
+ return false;
389
+ }
390
+ },
391
+ destroy: async (client) => {
392
+ try {
393
+ await client.end();
394
+ } catch (error) {
395
+ logger.error("SFTP destroy failed", { error });
396
+ }
397
+ }
398
+ },
399
+ {
400
+ max: 3,
401
+ min: 0,
402
+ idleTimeoutMillis: 6e4,
403
+ acquireTimeoutMillis: 1e4,
404
+ evictionRunIntervalMillis: 3e4,
405
+ testOnBorrow: true
406
+ }
407
+ );
408
+ };
290
409
 
291
410
  // src/server/infrastructure/cache/cache-key-delimiter.ts
292
411
  var CACHE_KEY_DELIMITER = "|";
@@ -1509,4 +1628,4 @@ function createSeoMetadataCommandRepository(prisma) {
1509
1628
  };
1510
1629
  }
1511
1630
 
1512
- export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCache, createCacheResult, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createIpRateLimiter, createJwtService, createPostCommandRepository, createPostQueryRepository, createR2Service, createSeoMetadataCommandRepository, normalizeCacheKey };
1631
+ export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCache, createCacheResult, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createIpRateLimiter, createJwtService, createPostCommandRepository, createPostQueryRepository, createR2Service, createSeoMetadataCommandRepository, createSftpPool, createSftpService, normalizeCacheKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yimingliao/cms",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "author": "Yiming Liao",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,20 +35,31 @@
35
35
  "ulid": "^3.0.2"
36
36
  },
37
37
  "devDependencies": {
38
+ "@aws-sdk/client-s3": "^3.1004.0",
38
39
  "@prisma/client": "6.5.0",
39
40
  "@types/jsonwebtoken": "^9.0.10",
40
41
  "@types/mime-types": "^3.0.1",
42
+ "@types/ssh2-sftp-client": "^9.0.6",
43
+ "generic-pool": "^3.9.0",
41
44
  "next": "^16.1.6",
42
45
  "prisma": "6.5.0",
43
46
  "tsup": "^8.5.1",
44
47
  "typescript": "^5.9.3"
45
48
  },
46
49
  "peerDependencies": {
47
- "@aws-sdk/client-s3": "^3.0.0"
50
+ "@aws-sdk/client-s3": "^3.0.0",
51
+ "generic-pool": "^3.9.0",
52
+ "ssh2-sftp-client": "^12.1.0"
48
53
  },
49
54
  "peerDependenciesMeta": {
50
55
  "@aws-sdk/client-s3": {
51
56
  "optional": true
57
+ },
58
+ "generic-pool": {
59
+ "optional": true
60
+ },
61
+ "ssh2-sftp-client": {
62
+ "optional": true
52
63
  }
53
64
  }
54
65
  }