@airoom/nextmin-node 1.3.0 → 1.4.1
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/dist/api/router/setupFileRoutes.js +2 -23
- package/dist/files/FileStorageAdapter.d.ts +2 -0
- package/dist/files/LocalFileStorageAdapter.d.ts +16 -0
- package/dist/files/LocalFileStorageAdapter.js +58 -0
- package/dist/files/S3FileStorageAdapter.js +11 -5
- package/dist/files/filename.d.ts +10 -0
- package/dist/files/filename.js +35 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/BaseModel.d.ts +1 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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 */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FileStorageAdapter, UploadResult, UploadPayload, FileProvider } from './FileStorageAdapter';
|
|
2
|
+
export interface LocalFileStorageOptions {
|
|
3
|
+
uploadDir: string;
|
|
4
|
+
publicUrlPrefix: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class LocalFileStorageAdapter implements FileStorageAdapter {
|
|
7
|
+
readonly name: FileProvider;
|
|
8
|
+
private uploadDir;
|
|
9
|
+
private publicUrlPrefix;
|
|
10
|
+
constructor(options: LocalFileStorageOptions);
|
|
11
|
+
upload(input: UploadPayload): Promise<UploadResult>;
|
|
12
|
+
delete(key: string): Promise<{
|
|
13
|
+
deleted: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
getPublicUrl(key: string): string | null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LocalFileStorageAdapter = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const stream_1 = require("stream");
|
|
10
|
+
const filename_1 = require("./filename");
|
|
11
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
class LocalFileStorageAdapter {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.name = 'local';
|
|
15
|
+
this.uploadDir = options.uploadDir;
|
|
16
|
+
this.publicUrlPrefix = options.publicUrlPrefix.replace(/\/$/, '');
|
|
17
|
+
if (!fs_1.default.existsSync(this.uploadDir)) {
|
|
18
|
+
fs_1.default.mkdirSync(this.uploadDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async upload(input) {
|
|
22
|
+
const originalName = input.originalFilename || 'unknown-file';
|
|
23
|
+
const randomId = crypto_1.default.randomBytes(4).toString('hex');
|
|
24
|
+
const uniqueName = input.key || (0, filename_1.filenameWithRandomId)(originalName, randomId);
|
|
25
|
+
const filePath = path_1.default.join(this.uploadDir, uniqueName);
|
|
26
|
+
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
27
|
+
// Convert body to stream if it is a Buffer
|
|
28
|
+
const readStream = new stream_1.Readable();
|
|
29
|
+
readStream.push(input.body);
|
|
30
|
+
readStream.push(null);
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
readStream.pipe(writeStream);
|
|
33
|
+
readStream.on('error', reject);
|
|
34
|
+
writeStream.on('finish', () => resolve());
|
|
35
|
+
writeStream.on('error', reject);
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
provider: 'local',
|
|
39
|
+
key: uniqueName,
|
|
40
|
+
url: `${this.publicUrlPrefix}/${uniqueName}`,
|
|
41
|
+
bucket: this.uploadDir,
|
|
42
|
+
size: input.body.byteLength,
|
|
43
|
+
contentType: input.contentType,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async delete(key) {
|
|
47
|
+
const filePath = path_1.default.join(this.uploadDir, key);
|
|
48
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
49
|
+
await fs_1.default.promises.unlink(filePath);
|
|
50
|
+
return { deleted: true };
|
|
51
|
+
}
|
|
52
|
+
return { deleted: false };
|
|
53
|
+
}
|
|
54
|
+
getPublicUrl(key) {
|
|
55
|
+
return `${this.publicUrlPrefix}/${key}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.LocalFileStorageAdapter = LocalFileStorageAdapter;
|
|
@@ -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
|
|
80
|
-
//
|
|
81
|
-
|
|
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;
|
package/dist/files/filename.d.ts
CHANGED
|
@@ -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;
|
package/dist/files/filename.js
CHANGED
|
@@ -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/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { MongoAdapter } from './database/MongoAdapter';
|
|
|
3
3
|
export { generateApiKey } from './utils/apiKey';
|
|
4
4
|
export * from './files/FileStorageAdapter';
|
|
5
5
|
export * from './files/S3FileStorageAdapter';
|
|
6
|
+
export * from './files/LocalFileStorageAdapter';
|
|
6
7
|
export * from './files/filename';
|
|
7
8
|
/**
|
|
8
9
|
* Compose the public router:
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ var apiKey_1 = require("./utils/apiKey");
|
|
|
28
28
|
Object.defineProperty(exports, "generateApiKey", { enumerable: true, get: function () { return apiKey_1.generateApiKey; } });
|
|
29
29
|
__exportStar(require("./files/FileStorageAdapter"), exports);
|
|
30
30
|
__exportStar(require("./files/S3FileStorageAdapter"), exports);
|
|
31
|
+
__exportStar(require("./files/LocalFileStorageAdapter"), exports);
|
|
31
32
|
__exportStar(require("./files/filename"), exports);
|
|
32
33
|
function isMultipart(req) {
|
|
33
34
|
const ct = (req.headers['content-type'] || '').toLowerCase();
|
package/package.json
CHANGED