@airoom/nextmin-node 1.3.0 → 1.4.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.
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.setupFileRoutes = setupFileRoutes;
7
7
  const multer_1 = __importDefault(require("multer"));
8
- const filename_1 = require("../../files/filename");
9
8
  function setupFileRoutes(ctx) {
10
9
  if (!ctx.fileStorage)
11
10
  return;
@@ -24,13 +23,8 @@ function setupFileRoutes(ctx) {
24
23
  .json({ error: true, message: 'No files uploaded' });
25
24
  }
26
25
  const results = await Promise.all(files.map(async (f) => {
27
- const folder = shortFolder();
28
- const ext = (f.originalname.match(/\.([A-Za-z0-9]{1,8})$/)?.[1] ??
29
- (0, filename_1.extFromMime)(f.mimetype) ??
30
- 'bin').toLowerCase();
31
- const key = `${folder}/${shortUid()}.${ext}`;
32
26
  const out = await ctx.fileStorage.upload({
33
- key,
27
+ originalFilename: f.originalname,
34
28
  body: f.buffer,
35
29
  contentType: f.mimetype,
36
30
  metadata: { originalName: f.originalname || '' },
@@ -68,13 +62,8 @@ function setupFileRoutes(ctx) {
68
62
  .json({ error: true, message: 'No files uploaded' });
69
63
  }
70
64
  const results = await Promise.all(files.map(async (f) => {
71
- const folder = shortFolder();
72
- const ext = (f.originalname.match(/\.([A-Za-z0-9]{1,8})$/)?.[1] ??
73
- (0, filename_1.extFromMime)(f.mimetype) ??
74
- 'bin').toLowerCase();
75
- const key = `${folder}/${shortUid()}.${ext}`;
76
65
  const out = await ctx.fileStorage.upload({
77
- key,
66
+ originalFilename: f.originalname,
78
67
  body: f.buffer,
79
68
  contentType: f.mimetype,
80
69
  metadata: { originalName: f.originalname || '' },
@@ -126,13 +115,3 @@ function setupFileRoutes(ctx) {
126
115
  }
127
116
  });
128
117
  }
129
- function shortFolder() {
130
- const d = new Date();
131
- const y = d.getFullYear();
132
- const m = String(d.getMonth() + 1).padStart(2, '0');
133
- const day = String(d.getDate()).padStart(2, '0');
134
- return `uploads/${y}/${m}/${day}`;
135
- }
136
- function shortUid() {
137
- return (Date.now().toString(36) + Math.random().toString(36).slice(2, 6)).toLowerCase();
138
- }
@@ -2,6 +2,8 @@ export type FileProvider = 's3' | 'gcs' | 'local' | (string & {});
2
2
  export interface UploadPayload {
3
3
  /** Path-like key inside bucket/provider, e.g. "uploads/userId/2025/08/18/file.png" */
4
4
  key?: string;
5
+ /** Original filename for slugification (e.g., "My Document.pdf") */
6
+ originalFilename?: string;
5
7
  /** Raw file content */
6
8
  body: Buffer | Uint8Array | ArrayBuffer;
7
9
  /** MIME type */
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.S3FileStorageAdapter = void 0;
4
4
  const client_s3_1 = require("@aws-sdk/client-s3");
5
+ const filename_1 = require("./filename");
5
6
  // Encode per path segment, keep slashes
6
7
  function encodeKeySegments(key) {
7
8
  return key.split('/').map(encodeURIComponent).join('/');
@@ -25,7 +26,7 @@ class S3FileStorageAdapter {
25
26
  async upload(input) {
26
27
  if (!input.body)
27
28
  throw new Error('S3 upload: "body" is required');
28
- const key = input.key ?? this.randomKey();
29
+ const key = input.key ?? this.randomKey(input.originalFilename);
29
30
  // Build params without ACL by default
30
31
  const params = {
31
32
  Bucket: this.bucket,
@@ -71,14 +72,19 @@ class S3FileStorageAdapter {
71
72
  return `https://${this.bucket}.s3.${this.regionStr}.amazonaws.com/${encKey}`;
72
73
  }
73
74
  // ---- private helpers ----
74
- randomKey() {
75
+ randomKey(originalFilename) {
75
76
  const ts = new Date();
76
77
  const y = ts.getUTCFullYear();
77
78
  const m = String(ts.getUTCMonth() + 1).padStart(2, '0');
78
79
  const d = String(ts.getUTCDate()).padStart(2, '0');
79
- const rand = Math.random().toString(36).slice(2, 10);
80
- // You wanted keys like uploads/YYYY/MM/DD/uid.ext (ext is added by router)
81
- return `uploads/${y}/${m}/${d}/${rand}`;
80
+ const randomId = Math.random().toString(36).slice(2, 10);
81
+ // Generate filename: slugified-name-randomid.ext or just randomid if no original name
82
+ const filename = originalFilename
83
+ ? (0, filename_1.filenameWithRandomId)(originalFilename, randomId)
84
+ : randomId;
85
+ // Path: uploads/YYYY/MM/DD/filename
86
+ const fullPath = `uploads/${y}/${m}/${d}/${filename}`;
87
+ return fullPath;
82
88
  }
83
89
  }
84
90
  exports.S3FileStorageAdapter = S3FileStorageAdapter;
@@ -1,4 +1,14 @@
1
1
  export declare function sanitizeFilename(name: string): string;
2
+ /**
3
+ * Slugify a filename: lowercase, replace spaces/special chars with hyphens
4
+ * Example: "My Photo (2024).jpg" → "my-photo-2024.jpg"
5
+ */
6
+ export declare function slugify(text: string): string;
7
+ /**
8
+ * Generate a filename from original name + random ID
9
+ * Example: "my-document.pdf" + "abc123" → "my-document-abc123.pdf"
10
+ */
11
+ export declare function filenameWithRandomId(originalName: string, randomId: string): string;
2
12
  export declare function withTimestampPrefix(name: string): string;
3
13
  export declare function joinKey(...parts: string[]): string;
4
14
  export declare function ensureExt(name: string, fallbackExt?: string): string;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanitizeFilename = sanitizeFilename;
4
+ exports.slugify = slugify;
5
+ exports.filenameWithRandomId = filenameWithRandomId;
4
6
  exports.withTimestampPrefix = withTimestampPrefix;
5
7
  exports.joinKey = joinKey;
6
8
  exports.ensureExt = ensureExt;
@@ -9,6 +11,39 @@ function sanitizeFilename(name) {
9
11
  const base = name.replace(/[/\\?%*:|"<>]/g, '-').replace(/\s+/g, '-');
10
12
  return base.replace(/-+/g, '-').toLowerCase();
11
13
  }
14
+ /**
15
+ * Slugify a filename: lowercase, replace spaces/special chars with hyphens
16
+ * Example: "My Photo (2024).jpg" → "my-photo-2024.jpg"
17
+ */
18
+ function slugify(text) {
19
+ return text
20
+ .toLowerCase()
21
+ .trim()
22
+ // Replace spaces and underscores with hyphens
23
+ .replace(/[\s_]+/g, '-')
24
+ // Remove special characters except hyphens, dots, and alphanumeric
25
+ .replace(/[^\w\-\.]+/g, '')
26
+ // Replace multiple consecutive hyphens with single hyphen
27
+ .replace(/-+/g, '-')
28
+ // Remove leading/trailing hyphens
29
+ .replace(/^-+|-+$/g, '');
30
+ }
31
+ /**
32
+ * Generate a filename from original name + random ID
33
+ * Example: "my-document.pdf" + "abc123" → "my-document-abc123.pdf"
34
+ */
35
+ function filenameWithRandomId(originalName, randomId) {
36
+ // Extract extension
37
+ const lastDot = originalName.lastIndexOf('.');
38
+ const hasExt = lastDot > 0 && lastDot < originalName.length - 1;
39
+ const nameWithoutExt = hasExt ? originalName.slice(0, lastDot) : originalName;
40
+ const ext = hasExt ? originalName.slice(lastDot) : '';
41
+ // Slugify the name part
42
+ const slugifiedName = slugify(nameWithoutExt);
43
+ // Combine: slugified-name-randomid.ext
44
+ const result = `${slugifiedName}-${randomId}${ext}`;
45
+ return result;
46
+ }
12
47
  function withTimestampPrefix(name) {
13
48
  const ts = Date.now();
14
49
  return `${ts}-${name}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airoom/nextmin-node",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",