@cyberskill/shared 3.9.0 → 3.10.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.
- package/dist/config/lint-staged/index.js +1 -1
- package/dist/config/lint-staged/index.js.map +1 -1
- package/dist/config/vitest/vitest.e2e.js +1 -1
- package/dist/config/vitest/vitest.unit.js +1 -1
- package/dist/node/express/express.util.js +3 -4
- package/dist/node/express/express.util.js.map +1 -1
- package/dist/node/fs/fs.util.js +14 -14
- package/dist/node/mongo/index.js +5 -5
- package/dist/node/mongo/mongo.controller.mongoose.js +86 -85
- package/dist/node/mongo/mongo.controller.mongoose.js.map +1 -1
- package/dist/node/mongo/mongo.dynamic-populate.d.ts +0 -4
- package/dist/node/mongo/mongo.dynamic-populate.js +14 -17
- package/dist/node/mongo/mongo.dynamic-populate.js.map +1 -1
- package/dist/node/mongo/mongo.util.js +2 -0
- package/dist/node/mongo/mongo.util.js.map +1 -1
- package/dist/node/storage/storage.util.js +41 -35
- package/dist/node/storage/storage.util.js.map +1 -1
- package/dist/node/upload/upload.util.js +34 -28
- package/dist/node/upload/upload.util.js.map +1 -1
- package/dist/node_modules/.pnpm/vitest@4.1.0_@types_node@25.5.0_jsdom@29.0.0_@noble_hashes@1.8.0__vite@8.0.1_@types_nod_36acd00c2670b611b011192023dc5bd9/node_modules/vitest/dist/config.js +8 -0
- package/dist/node_modules/.pnpm/{vitest@4.1.0_@types_node@25.5.0_jsdom@29.0.0_@noble_hashes@1.8.0__vite@8.0.0_@types_nod_53aa4254f295b3c40bb8f17b6ab226b5 → vitest@4.1.0_@types_node@25.5.0_jsdom@29.0.0_@noble_hashes@1.8.0__vite@8.0.1_@types_nod_36acd00c2670b611b011192023dc5bd9}/node_modules/vitest/dist/config.js.map +1 -1
- package/package.json +8 -8
- package/dist/node_modules/.pnpm/vitest@4.1.0_@types_node@25.5.0_jsdom@29.0.0_@noble_hashes@1.8.0__vite@8.0.0_@types_nod_53aa4254f295b3c40bb8f17b6ab226b5/node_modules/vitest/dist/config.js +0 -8
|
@@ -5,36 +5,36 @@ import s from "localforage";
|
|
|
5
5
|
import c from "node:path";
|
|
6
6
|
import l from "node:fs/promises";
|
|
7
7
|
//#region src/node/storage/storage.util.ts
|
|
8
|
-
var u = { baseDir: "" };
|
|
9
|
-
function
|
|
8
|
+
var u = 200, d = { baseDir: "" };
|
|
9
|
+
function f(e) {
|
|
10
10
|
return `${encodeURIComponent(e)}${a}`;
|
|
11
11
|
}
|
|
12
|
-
function
|
|
12
|
+
function p(e) {
|
|
13
13
|
return decodeURIComponent(e.slice(0, -a.length));
|
|
14
14
|
}
|
|
15
|
-
function
|
|
16
|
-
return c.join(t,
|
|
15
|
+
function m(e, t) {
|
|
16
|
+
return c.join(t, f(e));
|
|
17
17
|
}
|
|
18
|
-
var
|
|
18
|
+
var h = {
|
|
19
19
|
_driver: r,
|
|
20
20
|
_support: !0,
|
|
21
21
|
async _initStorage(t) {
|
|
22
22
|
try {
|
|
23
23
|
let n = typeof t == "object" && t && "baseDir" in t ? t.baseDir : void 0;
|
|
24
|
-
typeof n == "string" && n.length > 0 ?
|
|
24
|
+
typeof n == "string" && n.length > 0 ? d.baseDir = n : d.baseDir = e().CYBERSKILL_STORAGE_DIRECTORY, await l.mkdir(d.baseDir, { recursive: !0 });
|
|
25
25
|
} catch (e) {
|
|
26
26
|
throw n.error("[Storage:init]", e), e;
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
async clear() {
|
|
30
|
-
let { baseDir: e } =
|
|
30
|
+
let { baseDir: e } = d;
|
|
31
31
|
e && (await l.rm(e, {
|
|
32
32
|
recursive: !0,
|
|
33
33
|
force: !0
|
|
34
34
|
}), await l.mkdir(e, { recursive: !0 }));
|
|
35
35
|
},
|
|
36
36
|
async getItem(e) {
|
|
37
|
-
let { baseDir: t } =
|
|
37
|
+
let { baseDir: t } = d, n = m(e, t);
|
|
38
38
|
try {
|
|
39
39
|
let e = await l.readFile(n, "utf8");
|
|
40
40
|
return JSON.parse(e);
|
|
@@ -44,71 +44,77 @@ var m = {
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
async iterate(e) {
|
|
47
|
-
let t = await
|
|
47
|
+
let t = await h.keys(), n = 1;
|
|
48
48
|
for (let r of t) {
|
|
49
|
-
let t = e(await
|
|
49
|
+
let t = e(await h.getItem(r), r, n);
|
|
50
50
|
if (t !== void 0) return t;
|
|
51
51
|
n += 1;
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
async key(e) {
|
|
55
|
-
return (await
|
|
55
|
+
return (await h.keys())[e] ?? null;
|
|
56
56
|
},
|
|
57
57
|
async keys() {
|
|
58
|
-
let { baseDir: e } =
|
|
58
|
+
let { baseDir: e } = d;
|
|
59
59
|
try {
|
|
60
|
-
return (await l.readdir(e)).filter((e) => e.endsWith(a)).map(
|
|
60
|
+
return (await l.readdir(e)).filter((e) => e.endsWith(a)).map(p);
|
|
61
61
|
} catch (e) {
|
|
62
62
|
if (e.code === "ENOENT") return [];
|
|
63
63
|
throw e;
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
async length() {
|
|
67
|
-
return (await
|
|
67
|
+
return (await h.keys()).length;
|
|
68
68
|
},
|
|
69
69
|
async removeItem(e) {
|
|
70
|
-
let { baseDir: t } =
|
|
70
|
+
let { baseDir: t } = d, n = m(e, t);
|
|
71
71
|
await l.rm(n, { force: !0 });
|
|
72
72
|
},
|
|
73
73
|
async setItem(e, t) {
|
|
74
|
-
let { baseDir: n } =
|
|
74
|
+
let { baseDir: n } = d, r = m(e, n);
|
|
75
75
|
return await l.mkdir(n, { recursive: !0 }), await l.writeFile(r, JSON.stringify(t), "utf8"), t;
|
|
76
76
|
}
|
|
77
|
-
},
|
|
78
|
-
async function
|
|
79
|
-
return
|
|
80
|
-
await s.defineDriver(
|
|
77
|
+
}, g = null, _ = null;
|
|
78
|
+
async function v() {
|
|
79
|
+
return g ? (await g, _) : (g = (async () => {
|
|
80
|
+
await s.defineDriver(h), d.baseDir = e().CYBERSKILL_STORAGE_DIRECTORY;
|
|
81
81
|
let t = await s.getDriver(r);
|
|
82
82
|
await t._initStorage({
|
|
83
|
-
baseDir:
|
|
83
|
+
baseDir: d.baseDir,
|
|
84
84
|
name: i,
|
|
85
85
|
storeName: o
|
|
86
|
-
}),
|
|
86
|
+
}), _ = t;
|
|
87
87
|
})().catch((e) => {
|
|
88
|
-
throw
|
|
89
|
-
}), await
|
|
88
|
+
throw g = null, e;
|
|
89
|
+
}), await g, _);
|
|
90
90
|
}
|
|
91
|
-
var
|
|
91
|
+
var y = {
|
|
92
92
|
async get(e) {
|
|
93
|
+
if (e.length > u) return n.warn(`[Storage:get] Key exceeds maximum length of ${u} characters`), null;
|
|
93
94
|
try {
|
|
94
|
-
return await (await
|
|
95
|
+
return await (await v()).getItem(e) ?? null;
|
|
95
96
|
} catch (e) {
|
|
96
97
|
return t(e, { returnValue: null });
|
|
97
98
|
}
|
|
98
99
|
},
|
|
99
|
-
async set(e,
|
|
100
|
-
|
|
100
|
+
async set(e, n) {
|
|
101
|
+
if (e.length > u) throw RangeError(`Storage key exceeds maximum length of ${u} characters`);
|
|
102
|
+
try {
|
|
103
|
+
await (await v()).setItem(e, n);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
throw t(e), e;
|
|
106
|
+
}
|
|
101
107
|
},
|
|
102
108
|
async remove(e) {
|
|
103
109
|
try {
|
|
104
|
-
await (await
|
|
110
|
+
await (await v()).removeItem(e);
|
|
105
111
|
} catch (e) {
|
|
106
112
|
t(e);
|
|
107
113
|
}
|
|
108
114
|
},
|
|
109
115
|
async keys() {
|
|
110
116
|
try {
|
|
111
|
-
let e = await (await
|
|
117
|
+
let e = await (await v()).keys();
|
|
112
118
|
return Array.isArray(e) ? e : (n.warn("[Storage:keys] Invalid keys response:", e), []);
|
|
113
119
|
} catch (e) {
|
|
114
120
|
return t(e, { returnValue: [] });
|
|
@@ -116,16 +122,16 @@ var v = {
|
|
|
116
122
|
},
|
|
117
123
|
async getLogLink(n) {
|
|
118
124
|
try {
|
|
119
|
-
return `${c.join(e().CYBERSKILL_STORAGE_DIRECTORY,
|
|
125
|
+
return `${c.join(e().CYBERSKILL_STORAGE_DIRECTORY, f(n))} (key: ${n})`;
|
|
120
126
|
} catch (e) {
|
|
121
127
|
return t(e, { returnValue: null });
|
|
122
128
|
}
|
|
123
129
|
}
|
|
124
130
|
};
|
|
125
|
-
function
|
|
126
|
-
|
|
131
|
+
function b() {
|
|
132
|
+
g = null, _ = null, d.baseDir = "";
|
|
127
133
|
}
|
|
128
134
|
//#endregion
|
|
129
|
-
export {
|
|
135
|
+
export { b as resetStorageForTesting, y as storage };
|
|
130
136
|
|
|
131
137
|
//# sourceMappingURL=storage.util.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.util.js","names":[],"sources":["../../../src/node/storage/storage.util.ts"],"sourcesContent":["import localForage from 'localforage';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { getEnv } from '#config/env/index.js';\n\nimport type { LocalForageDriver, NodeFsDriverState, NodeLocalForageOptions } from './storage.type.js';\n\nimport { catchError, log } from '../log/index.js';\nimport { NODE_FS_DRIVER_NAME, STORAGE_INSTANCE_NAME, STORAGE_KEY_EXTENSION, STORAGE_STORE_NAME } from './storage.constant.js';\n\nconst nodeFsDriverState: NodeFsDriverState = {\n baseDir: '',\n};\n\n/**\n * Encodes a storage key into a filename-safe string.\n */\nfunction encodeKey(key: string): string {\n return `${encodeURIComponent(key)}${STORAGE_KEY_EXTENSION}`;\n}\n\n/**\n * Decodes a filename-safe key back to the original storage key.\n */\nfunction decodeKey(fileName: string): string {\n return decodeURIComponent(fileName.slice(0, -STORAGE_KEY_EXTENSION.length));\n}\n\n/**\n * Maps a storage key to an absolute file path inside the storage directory.\n */\nfunction getFilePath(key: string, baseDir: string): string {\n return path.join(baseDir, encodeKey(key));\n}\n\n/**\n * Custom localForage driver that stores JSON-encoded values on the filesystem.\n */\nconst nodeFsDriver: LocalForageDriver = {\n _driver: NODE_FS_DRIVER_NAME,\n _support: true,\n /** Ensures the storage directory exists and respects custom baseDir overrides. */\n async _initStorage(options: unknown) {\n try {\n const baseDirFromOptions = typeof options === 'object' && options !== null && 'baseDir' in options\n ? (options as { baseDir?: unknown }).baseDir\n : undefined;\n\n if (typeof baseDirFromOptions === 'string' && baseDirFromOptions.length > 0) {\n nodeFsDriverState.baseDir = baseDirFromOptions;\n }\n else {\n nodeFsDriverState.baseDir = getEnv().CYBERSKILL_STORAGE_DIRECTORY;\n }\n\n await fs.mkdir(nodeFsDriverState.baseDir, { recursive: true });\n }\n catch (error) {\n log.error('[Storage:init]', error);\n throw error;\n }\n },\n /** Deletes all stored entries by recreating the directory. Callers must ensure no concurrent operations. */\n async clear() {\n const { baseDir } = nodeFsDriverState;\n\n if (!baseDir) {\n return;\n }\n\n await fs.rm(baseDir, { recursive: true, force: true });\n await fs.mkdir(baseDir, { recursive: true });\n },\n /** Reads and parses a stored value; returns null when the file is missing. */\n async getItem<T>(key: string): Promise<T | null> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n try {\n const content = await fs.readFile(filePath, 'utf8');\n\n return JSON.parse(content) as T;\n }\n catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return null;\n }\n throw error;\n }\n },\n /** Iterates through all keys, invoking the iterator until it returns a value. */\n async iterate<T, U>(iterator: (value: T, key: string, iterationNumber: number) => U): Promise<U> {\n const keys = await nodeFsDriver.keys();\n let iterationNumber = 1;\n\n for (const key of keys) {\n const value = await nodeFsDriver.getItem<T>(key);\n\n const result = iterator(value as T, key, iterationNumber);\n\n if (result !== undefined) {\n return result;\n }\n iterationNumber += 1;\n }\n\n return undefined as unknown as U;\n },\n /** Returns the key name at the given index or null when out of bounds. */\n async key(keyIndex: number): Promise<string> {\n const keys = await nodeFsDriver.keys();\n\n return keys[keyIndex] ?? null as unknown as string;\n },\n /** Lists all stored keys. */\n async keys(): Promise<string[]> {\n const { baseDir } = nodeFsDriverState;\n\n try {\n const files = await fs.readdir(baseDir);\n\n return files\n .filter(file => file.endsWith(STORAGE_KEY_EXTENSION))\n .map(decodeKey);\n }\n catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return [];\n }\n throw error;\n }\n },\n /** Returns the count of stored keys. */\n async length(): Promise<number> {\n const keys = await nodeFsDriver.keys();\n\n return keys.length;\n },\n /** Removes a stored value for the given key. */\n async removeItem(key: string): Promise<void> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n await fs.rm(filePath, { force: true });\n },\n /** Stores a value as JSON on disk. */\n async setItem<T>(key: string, value: T): Promise<T> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n await fs.mkdir(baseDir, { recursive: true });\n await fs.writeFile(filePath, JSON.stringify(value), 'utf8');\n\n return value;\n },\n};\n\nlet initPromise: Promise<void> | null = null;\nlet driverInstance: LocalForageDriver | null = null;\n\n/**\n * Prepares and returns the filesystem-backed localForage driver.\n * We bypass localForage's default driver selection and explicitly initialize\n * our custom driver to ensure Node compatibility.\n */\nasync function ensureLocalForageReady(): Promise<LocalForageDriver> {\n if (initPromise) {\n await initPromise;\n return driverInstance as LocalForageDriver;\n }\n\n initPromise = (async () => {\n await localForage.defineDriver(nodeFsDriver);\n nodeFsDriverState.baseDir = getEnv().CYBERSKILL_STORAGE_DIRECTORY;\n const driver = await localForage.getDriver(NODE_FS_DRIVER_NAME);\n\n const initOptions = {\n baseDir: nodeFsDriverState.baseDir,\n name: STORAGE_INSTANCE_NAME,\n storeName: STORAGE_STORE_NAME,\n } satisfies NodeLocalForageOptions;\n\n await driver._initStorage(initOptions as unknown as Parameters<typeof driver._initStorage>[0]);\n driverInstance = driver;\n })().catch((error) => {\n initPromise = null;\n throw error;\n });\n\n await initPromise;\n\n return driverInstance as LocalForageDriver;\n}\n\n/**\n * Persistent storage utility object for data persistence across application sessions.\n * This object provides methods for storing, retrieving, and managing data using localForage,\n * with automatic initialization and error handling.\n */\nexport const storage = {\n /**\n * Retrieves a value from persistent storage by key.\n * This method fetches data that was previously stored using the set method.\n * Returns null if the key doesn't exist or if an error occurs.\n *\n * @param key - The unique identifier for the stored value.\n * @returns A promise that resolves to the stored value or null if not found.\n */\n async get<T = unknown>(key: string): Promise<T | null> {\n try {\n const driver = await ensureLocalForageReady();\n const result = await driver.getItem<T>(key);\n\n return result ?? null;\n }\n catch (error) {\n return catchError(error, { returnValue: null });\n }\n },\n /**\n * Stores a value in persistent storage with a unique key.\n * This method saves data that can be retrieved later using the get method.\n * The data is automatically serialized and stored in the configured storage directory.\n *\n * @param key - The unique identifier for the value to store.\n * @param value - The data to store (will be automatically serialized).\n * @returns A promise that resolves when the storage operation is complete.\n */\n async set<T = unknown>(key: string, value: T): Promise<void> {\n const driver = await ensureLocalForageReady();\n\n await driver.setItem(key, value);\n },\n /**\n * Removes a value from persistent storage by key.\n * This method permanently deletes the stored data associated with the specified key.\n *\n * @param key - The unique identifier of the value to remove.\n * @returns A promise that resolves when the removal operation is complete.\n */\n async remove(key: string): Promise<void> {\n try {\n const driver = await ensureLocalForageReady();\n\n await driver.removeItem(key);\n }\n catch (error) {\n catchError(error);\n }\n },\n /**\n * Retrieves all storage keys.\n * This method returns an array of all keys that currently have stored values.\n * Returns an empty array if no keys exist or if an error occurs.\n *\n * @returns A promise that resolves to an array of storage keys.\n */\n async keys(): Promise<string[]> {\n try {\n const driver = await ensureLocalForageReady();\n const keys = await driver.keys();\n\n if (!Array.isArray(keys)) {\n log.warn(`[Storage:keys] Invalid keys response:`, keys);\n return [];\n }\n\n return keys;\n }\n catch (error) {\n return catchError(error, { returnValue: [] });\n }\n },\n /**\n * Gets a human-readable log link for a storage key.\n * This method provides a formatted string that shows the storage directory path\n * and the key name for debugging and manual inspection purposes.\n *\n * @param key - The storage key to generate a log link for.\n * @returns A promise that resolves to a formatted log link string or null if an error occurs.\n */\n async getLogLink(key: string): Promise<string | null> {\n try {\n const storagePath = path.join(getEnv().CYBERSKILL_STORAGE_DIRECTORY, encodeKey(key));\n\n return `${storagePath} (key: ${key})`;\n }\n catch (error) {\n return catchError(error, { returnValue: null });\n }\n },\n\n};\n\n/**\n * Resets all module-level singleton state used by the storage module.\n * Intended for use in tests to ensure isolation between test cases.\n * Do NOT call this in production code.\n */\nexport function resetStorageForTesting(): void {\n initPromise = null;\n driverInstance = null;\n nodeFsDriverState.baseDir = '';\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storage.util.js","names":[],"sources":["../../../src/node/storage/storage.util.ts"],"sourcesContent":["import localForage from 'localforage';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { getEnv } from '#config/env/index.js';\n\nimport type { LocalForageDriver, NodeFsDriverState, NodeLocalForageOptions } from './storage.type.js';\n\nimport { catchError, log } from '../log/index.js';\nimport { NODE_FS_DRIVER_NAME, STORAGE_INSTANCE_NAME, STORAGE_KEY_EXTENSION, STORAGE_STORE_NAME } from './storage.constant.js';\n\nconst MAX_KEY_LENGTH = 200;\n\nconst nodeFsDriverState: NodeFsDriverState = {\n baseDir: '',\n};\n\n/**\n * Encodes a storage key into a filename-safe string.\n */\nfunction encodeKey(key: string): string {\n return `${encodeURIComponent(key)}${STORAGE_KEY_EXTENSION}`;\n}\n\n/**\n * Decodes a filename-safe key back to the original storage key.\n */\nfunction decodeKey(fileName: string): string {\n return decodeURIComponent(fileName.slice(0, -STORAGE_KEY_EXTENSION.length));\n}\n\n/**\n * Maps a storage key to an absolute file path inside the storage directory.\n */\nfunction getFilePath(key: string, baseDir: string): string {\n return path.join(baseDir, encodeKey(key));\n}\n\n/**\n * Custom localForage driver that stores JSON-encoded values on the filesystem.\n */\nconst nodeFsDriver: LocalForageDriver = {\n _driver: NODE_FS_DRIVER_NAME,\n _support: true,\n /** Ensures the storage directory exists and respects custom baseDir overrides. */\n async _initStorage(options: unknown) {\n try {\n const baseDirFromOptions = typeof options === 'object' && options !== null && 'baseDir' in options\n ? (options as { baseDir?: unknown }).baseDir\n : undefined;\n\n if (typeof baseDirFromOptions === 'string' && baseDirFromOptions.length > 0) {\n nodeFsDriverState.baseDir = baseDirFromOptions;\n }\n else {\n nodeFsDriverState.baseDir = getEnv().CYBERSKILL_STORAGE_DIRECTORY;\n }\n\n await fs.mkdir(nodeFsDriverState.baseDir, { recursive: true });\n }\n catch (error) {\n log.error('[Storage:init]', error);\n throw error;\n }\n },\n /** Deletes all stored entries by recreating the directory. Callers must ensure no concurrent operations. */\n async clear() {\n const { baseDir } = nodeFsDriverState;\n\n if (!baseDir) {\n return;\n }\n\n await fs.rm(baseDir, { recursive: true, force: true });\n await fs.mkdir(baseDir, { recursive: true });\n },\n /** Reads and parses a stored value; returns null when the file is missing. */\n async getItem<T>(key: string): Promise<T | null> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n try {\n const content = await fs.readFile(filePath, 'utf8');\n\n return JSON.parse(content) as T;\n }\n catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return null;\n }\n throw error;\n }\n },\n /** Iterates through all keys, invoking the iterator until it returns a value. */\n async iterate<T, U>(iterator: (value: T, key: string, iterationNumber: number) => U): Promise<U> {\n const keys = await nodeFsDriver.keys();\n let iterationNumber = 1;\n\n for (const key of keys) {\n const value = await nodeFsDriver.getItem<T>(key);\n\n const result = iterator(value as T, key, iterationNumber);\n\n if (result !== undefined) {\n return result;\n }\n iterationNumber += 1;\n }\n\n return undefined as unknown as U;\n },\n /** Returns the key name at the given index or null when out of bounds. */\n async key(keyIndex: number): Promise<string> {\n const keys = await nodeFsDriver.keys();\n\n return keys[keyIndex] ?? null as unknown as string;\n },\n /** Lists all stored keys. */\n async keys(): Promise<string[]> {\n const { baseDir } = nodeFsDriverState;\n\n try {\n const files = await fs.readdir(baseDir);\n\n return files\n .filter(file => file.endsWith(STORAGE_KEY_EXTENSION))\n .map(decodeKey);\n }\n catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return [];\n }\n throw error;\n }\n },\n /** Returns the count of stored keys. */\n async length(): Promise<number> {\n const keys = await nodeFsDriver.keys();\n\n return keys.length;\n },\n /** Removes a stored value for the given key. */\n async removeItem(key: string): Promise<void> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n await fs.rm(filePath, { force: true });\n },\n /** Stores a value as JSON on disk. */\n async setItem<T>(key: string, value: T): Promise<T> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n await fs.mkdir(baseDir, { recursive: true });\n await fs.writeFile(filePath, JSON.stringify(value), 'utf8');\n\n return value;\n },\n};\n\nlet initPromise: Promise<void> | null = null;\nlet driverInstance: LocalForageDriver | null = null;\n\n/**\n * Prepares and returns the filesystem-backed localForage driver.\n * We bypass localForage's default driver selection and explicitly initialize\n * our custom driver to ensure Node compatibility.\n */\nasync function ensureLocalForageReady(): Promise<LocalForageDriver> {\n if (initPromise) {\n await initPromise;\n return driverInstance as LocalForageDriver;\n }\n\n initPromise = (async () => {\n await localForage.defineDriver(nodeFsDriver);\n nodeFsDriverState.baseDir = getEnv().CYBERSKILL_STORAGE_DIRECTORY;\n const driver = await localForage.getDriver(NODE_FS_DRIVER_NAME);\n\n const initOptions = {\n baseDir: nodeFsDriverState.baseDir,\n name: STORAGE_INSTANCE_NAME,\n storeName: STORAGE_STORE_NAME,\n } satisfies NodeLocalForageOptions;\n\n await driver._initStorage(initOptions as unknown as Parameters<typeof driver._initStorage>[0]);\n driverInstance = driver;\n })().catch((error) => {\n initPromise = null;\n throw error;\n });\n\n await initPromise;\n\n return driverInstance as LocalForageDriver;\n}\n\n/**\n * Persistent storage utility object for data persistence across application sessions.\n * This object provides methods for storing, retrieving, and managing data using localForage,\n * with automatic initialization and error handling.\n */\nexport const storage = {\n /**\n * Retrieves a value from persistent storage by key.\n * This method fetches data that was previously stored using the set method.\n * Returns null if the key doesn't exist or if an error occurs.\n *\n * @param key - The unique identifier for the stored value.\n * @returns A promise that resolves to the stored value or null if not found.\n */\n async get<T = unknown>(key: string): Promise<T | null> {\n if (key.length > MAX_KEY_LENGTH) {\n log.warn(`[Storage:get] Key exceeds maximum length of ${MAX_KEY_LENGTH} characters`);\n return null;\n }\n\n try {\n const driver = await ensureLocalForageReady();\n const result = await driver.getItem<T>(key);\n\n return result ?? null;\n }\n catch (error) {\n return catchError(error, { returnValue: null });\n }\n },\n /**\n * Stores a value in persistent storage with a unique key.\n * This method saves data that can be retrieved later using the get method.\n * The data is automatically serialized and stored in the configured storage directory.\n *\n * @param key - The unique identifier for the value to store.\n * @param value - The data to store (will be automatically serialized).\n * @returns A promise that resolves when the storage operation is complete.\n */\n async set<T = unknown>(key: string, value: T): Promise<void> {\n if (key.length > MAX_KEY_LENGTH) {\n throw new RangeError(`Storage key exceeds maximum length of ${MAX_KEY_LENGTH} characters`);\n }\n\n try {\n const driver = await ensureLocalForageReady();\n\n await driver.setItem(key, value);\n }\n catch (error) {\n catchError(error);\n throw error;\n }\n },\n /**\n * Removes a value from persistent storage by key.\n * This method permanently deletes the stored data associated with the specified key.\n *\n * @param key - The unique identifier of the value to remove.\n * @returns A promise that resolves when the removal operation is complete.\n */\n async remove(key: string): Promise<void> {\n try {\n const driver = await ensureLocalForageReady();\n\n await driver.removeItem(key);\n }\n catch (error) {\n catchError(error);\n }\n },\n /**\n * Retrieves all storage keys.\n * This method returns an array of all keys that currently have stored values.\n * Returns an empty array if no keys exist or if an error occurs.\n *\n * @returns A promise that resolves to an array of storage keys.\n */\n async keys(): Promise<string[]> {\n try {\n const driver = await ensureLocalForageReady();\n const keys = await driver.keys();\n\n if (!Array.isArray(keys)) {\n log.warn(`[Storage:keys] Invalid keys response:`, keys);\n return [];\n }\n\n return keys;\n }\n catch (error) {\n return catchError(error, { returnValue: [] });\n }\n },\n /**\n * Gets a human-readable log link for a storage key.\n * This method provides a formatted string that shows the storage directory path\n * and the key name for debugging and manual inspection purposes.\n *\n * @param key - The storage key to generate a log link for.\n * @returns A promise that resolves to a formatted log link string or null if an error occurs.\n */\n async getLogLink(key: string): Promise<string | null> {\n try {\n const storagePath = path.join(getEnv().CYBERSKILL_STORAGE_DIRECTORY, encodeKey(key));\n\n return `${storagePath} (key: ${key})`;\n }\n catch (error) {\n return catchError(error, { returnValue: null });\n }\n },\n\n};\n\n/**\n * Resets all module-level singleton state used by the storage module.\n * Intended for use in tests to ensure isolation between test cases.\n * Do NOT call this in production code.\n */\nexport function resetStorageForTesting(): void {\n initPromise = null;\n driverInstance = null;\n nodeFsDriverState.baseDir = '';\n}\n"],"mappings":"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"}
|
|
@@ -4,10 +4,11 @@ import { BYTES_PER_MB as n, DEFAULT_UPLOAD_CONFIG as r } from "./upload.constant
|
|
|
4
4
|
import { createWriteStream as i, mkdirSync as a, pathExistsSync as o } from "../fs/fs.util.js";
|
|
5
5
|
import { dirname as s } from "../path/path.util.js";
|
|
6
6
|
import { Buffer as c } from "node:buffer";
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
7
|
+
import l from "node:path";
|
|
8
|
+
import { Transform as u } from "node:stream";
|
|
9
|
+
import { ReadableStream as d } from "node:stream/web";
|
|
9
10
|
//#region src/node/upload/upload.util.ts
|
|
10
|
-
async function
|
|
11
|
+
async function f(e) {
|
|
11
12
|
return new Promise((t, n) => {
|
|
12
13
|
let r = 0;
|
|
13
14
|
e.on("data", (e) => {
|
|
@@ -15,8 +16,8 @@ async function d(e) {
|
|
|
15
16
|
}), e.on("end", () => t(r)), e.on("error", n);
|
|
16
17
|
});
|
|
17
18
|
}
|
|
18
|
-
async function
|
|
19
|
-
let i = await (await n).file, a = await
|
|
19
|
+
async function p(t, n, r) {
|
|
20
|
+
let i = await (await n).file, a = await f(i.createReadStream()), o = r ?? _(), s = g({
|
|
20
21
|
filename: i.filename,
|
|
21
22
|
fileSize: a
|
|
22
23
|
}, o, t);
|
|
@@ -30,30 +31,30 @@ async function f(t, n, r) {
|
|
|
30
31
|
code: e.BAD_REQUEST.CODE
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
|
-
async function
|
|
34
|
-
let i = (r ??
|
|
34
|
+
async function m(e, t, r) {
|
|
35
|
+
let i = (r ?? _())[e], a = await p(e, t, r);
|
|
35
36
|
if (!a.success) return a;
|
|
36
|
-
let { createReadStream: o } = a.result, s = i.sizeLimit,
|
|
37
|
+
let { createReadStream: o } = a.result, s = i.sizeLimit, l = new u({ transform(e, t, r) {
|
|
37
38
|
s -= e.length, s < 0 ? r(/* @__PURE__ */ Error(`File size exceeds limit of ${i.sizeLimit / n}MB`)) : r(null, e);
|
|
38
|
-
} }),
|
|
39
|
+
} }), f = o().pipe(l);
|
|
39
40
|
return {
|
|
40
41
|
success: !0,
|
|
41
|
-
result: new
|
|
42
|
-
|
|
42
|
+
result: new d({ start(e) {
|
|
43
|
+
f.on("data", (t) => {
|
|
43
44
|
e.enqueue(typeof t == "string" ? c.from(t) : t);
|
|
44
|
-
}),
|
|
45
|
+
}), f.on("end", () => e.close()), f.on("error", (t) => e.error(t));
|
|
45
46
|
} })
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
|
-
function
|
|
49
|
+
function h(e, t) {
|
|
49
50
|
let n = e.lastIndexOf(".");
|
|
50
51
|
if (n === -1) return !1;
|
|
51
52
|
let r = e.substring(n + 1).toLowerCase();
|
|
52
53
|
return t.includes(r);
|
|
53
54
|
}
|
|
54
|
-
function
|
|
55
|
+
function g(e, t, n) {
|
|
55
56
|
let { filename: r, fileSize: i } = e, { allowedExtensions: a, sizeLimit: o } = t[n];
|
|
56
|
-
if (!
|
|
57
|
+
if (!h(r, a)) return {
|
|
57
58
|
isValid: !1,
|
|
58
59
|
error: `File extension not allowed for ${n.toLowerCase()} files. Allowed extensions: ${a.join(", ")}`
|
|
59
60
|
};
|
|
@@ -66,25 +67,30 @@ function h(e, t, n) {
|
|
|
66
67
|
}
|
|
67
68
|
return { isValid: !0 };
|
|
68
69
|
}
|
|
69
|
-
function
|
|
70
|
+
function _(e) {
|
|
70
71
|
return {
|
|
71
72
|
...r,
|
|
72
73
|
...e
|
|
73
74
|
};
|
|
74
75
|
}
|
|
75
|
-
async function
|
|
76
|
-
let { path: r, file: c, config:
|
|
76
|
+
async function v(n) {
|
|
77
|
+
let { path: r, file: c, config: u, type: d } = n;
|
|
77
78
|
if (!r || typeof r != "string") return {
|
|
78
79
|
success: !1,
|
|
79
80
|
message: "Invalid path provided",
|
|
80
81
|
code: e.BAD_REQUEST.CODE
|
|
81
82
|
};
|
|
83
|
+
if (l.resolve(r) !== l.normalize(r) && r.includes("..")) return {
|
|
84
|
+
success: !1,
|
|
85
|
+
message: "Path traversal detected: \"..\" segments are not allowed",
|
|
86
|
+
code: e.BAD_REQUEST.CODE
|
|
87
|
+
};
|
|
82
88
|
if (!c || typeof c != "object") return {
|
|
83
89
|
success: !1,
|
|
84
90
|
message: "Invalid file provided",
|
|
85
91
|
code: e.BAD_REQUEST.CODE
|
|
86
92
|
};
|
|
87
|
-
if (
|
|
93
|
+
if (u) {
|
|
88
94
|
let n = [
|
|
89
95
|
t.IMAGE,
|
|
90
96
|
t.VIDEO,
|
|
@@ -92,12 +98,12 @@ async function _(n) {
|
|
|
92
98
|
t.OTHER
|
|
93
99
|
];
|
|
94
100
|
for (let t of n) {
|
|
95
|
-
if (!
|
|
101
|
+
if (!u[t] || !Array.isArray(u[t].allowedExtensions) || u[t].allowedExtensions.length === 0) return {
|
|
96
102
|
success: !1,
|
|
97
103
|
message: `Invalid config for ${t.toLowerCase()} files`,
|
|
98
104
|
code: e.BAD_REQUEST.CODE
|
|
99
105
|
};
|
|
100
|
-
if (typeof
|
|
106
|
+
if (typeof u[t].sizeLimit != "number" || u[t].sizeLimit <= 0) return {
|
|
101
107
|
success: !1,
|
|
102
108
|
message: `Invalid size limit for ${t.toLowerCase()} files`,
|
|
103
109
|
code: e.BAD_REQUEST.CODE
|
|
@@ -105,13 +111,13 @@ async function _(n) {
|
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
try {
|
|
108
|
-
let t = await
|
|
114
|
+
let t = await p(d, await c, u);
|
|
109
115
|
if (!t.success) return t;
|
|
110
|
-
let { createReadStream: n } = t.result,
|
|
111
|
-
o(
|
|
112
|
-
let
|
|
113
|
-
return
|
|
114
|
-
m.on("finish", () => e()), m.on("error", t),
|
|
116
|
+
let { createReadStream: n } = t.result, l = s(r);
|
|
117
|
+
o(l) || a(l, { recursive: !0 });
|
|
118
|
+
let f = n(), m = i(r);
|
|
119
|
+
return f.pipe(m), await new Promise((e, t) => {
|
|
120
|
+
m.on("finish", () => e()), m.on("error", t), f.on("error", t);
|
|
115
121
|
}), {
|
|
116
122
|
success: !0,
|
|
117
123
|
result: r,
|
|
@@ -127,6 +133,6 @@ async function _(n) {
|
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
//#endregion
|
|
130
|
-
export {
|
|
136
|
+
export { _ as createUploadConfig, p as getAndValidateFile, f as getFileSizeFromStream, m as getFileWebStream, v as upload, h as validateFileExtension, g as validateUpload };
|
|
131
137
|
|
|
132
138
|
//# sourceMappingURL=upload.util.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.util.js","names":[],"sources":["../../../src/node/upload/upload.util.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\nimport { Transform } from 'node:stream';\nimport { ReadableStream } from 'node:stream/web';\n\nimport type { I_Return } from '#typescript/index.js';\n\nimport { RESPONSE_STATUS } from '#constant/index.js';\n\nimport type { I_UploadConfig, I_UploadFile, I_UploadFileData, I_UploadOptions, I_UploadTypeConfig, I_UploadValidationConfig } from './upload.type.js';\n\nimport { createWriteStream, mkdirSync, pathExistsSync } from '../fs/index.js';\nimport { dirname } from '../path/index.js';\nimport { BYTES_PER_MB, DEFAULT_UPLOAD_CONFIG } from './upload.constant.js';\nimport { E_UploadType } from './upload.type.js';\n\n/**\n * Calculates the size of a file from a readable stream.\n * This function reads through the entire stream to determine the total byte size\n * by accumulating the length of each data chunk.\n *\n * @param stream - The readable stream to calculate the size for.\n * @returns A promise that resolves to the total size of the stream in bytes.\n */\nexport async function getFileSizeFromStream(stream: NodeJS.ReadableStream): Promise<number> {\n return new Promise((resolve, reject) => {\n let size = 0;\n stream.on('data', (chunk) => {\n size += chunk.length;\n });\n stream.on('end', () => resolve(size));\n stream.on('error', reject);\n });\n}\n\n/**\n * Extracts and validates file data from an upload file.\n * This function processes upload files by:\n * - Extracting file metadata and creating a readable stream\n * - Calculating the file size from the stream\n * - Validating file size and extension against upload configuration\n * - Returning a standardized response with success status and error codes\n * - Providing validated file data for further processing\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing validated file data or error information.\n */\nexport async function getAndValidateFile(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<I_UploadFileData>> {\n const fileData = await (await file).file;\n const stream = fileData.createReadStream();\n // Stream is consumed here for validation; callers use createReadStream() again for the actual write.\n // This is intentional — createReadStream() is a factory that yields a new stream per call.\n const fileSize = await getFileSizeFromStream(stream);\n const uploadConfig = config ?? createUploadConfig();\n\n const validationResult = validateUpload(\n { filename: fileData.filename, fileSize },\n uploadConfig,\n type,\n );\n\n if (!validationResult.isValid) {\n return {\n success: false,\n message: validationResult.error || 'File validation failed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n return {\n success: true,\n result: fileData,\n message: 'File validated successfully',\n };\n}\n\n/**\n * Creates a validated web-readable stream from an upload file with size validation.\n * This function processes file uploads for web environments by:\n * - Validating file data using getAndValidateFile function\n * - Creating a size validation transform stream to monitor upload progress\n * - Returning a web-compatible ReadableStream with real-time validation\n * - Providing standardized error responses for validation failures\n * - Wrapping the stream in a standardized response format\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing either a web ReadableStream or error information.\n */\nexport async function getFileWebStream(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<ReadableStream<Uint8Array>>> {\n const uploadConfig = config ?? createUploadConfig();\n const typeConfig = uploadConfig[type];\n\n const fileData = await getAndValidateFile(type, file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n let remainingBytes = typeConfig.sizeLimit;\n\n const sizeValidationStream = new Transform({\n transform(chunk: Buffer, _enc: BufferEncoding, cb) {\n remainingBytes -= chunk.length;\n\n if (remainingBytes < 0) {\n cb(new Error(`File size exceeds limit of ${typeConfig.sizeLimit / BYTES_PER_MB}MB`));\n }\n else {\n cb(null, chunk);\n }\n },\n });\n const originalStream = createReadStream();\n const validatedStream = originalStream.pipe(sizeValidationStream);\n\n return {\n success: true,\n result: new ReadableStream<Uint8Array>({\n start(controller) {\n validatedStream.on('data', (chunk: Buffer | string) => {\n controller.enqueue(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n });\n validatedStream.on('end', () => controller.close());\n validatedStream.on('error', (err: unknown) => controller.error(err));\n },\n }),\n };\n}\n\n/**\n * Validates if a file has an allowed extension.\n * This function extracts the file extension from the filename and checks if it's\n * included in the list of allowed extensions (case-insensitive comparison).\n *\n * @param filename - The filename to check for valid extension.\n * @param allowedExtensions - An array of allowed file extensions (without dots).\n * @returns True if the file extension is allowed, false otherwise.\n */\nexport function validateFileExtension(filename: string, allowedExtensions: string[]): boolean {\n const lastDotIndex = filename.lastIndexOf('.');\n\n if (lastDotIndex === -1) {\n return false;\n }\n\n const extension = filename.substring(lastDotIndex + 1).toLowerCase();\n\n return allowedExtensions.includes(extension);\n}\n\n/**\n * Validates an upload against the specified configuration.\n * This function performs comprehensive validation including:\n * - File extension validation against allowed extensions\n * - File size validation against size limits\n * - Returns detailed error messages for validation failures\n *\n * @param config - The validation configuration including filename and optional file size.\n * @param uploadConfig - The upload configuration containing allowed extensions and size limits.\n * @param uploadType - The type of upload being validated.\n * @returns An object indicating validation success and optional error message.\n */\nexport function validateUpload(\n config: I_UploadValidationConfig,\n uploadConfig: I_UploadConfig,\n uploadType: E_UploadType,\n): { isValid: boolean; error?: string } {\n const { filename, fileSize } = config;\n const typeConfig: I_UploadTypeConfig = uploadConfig[uploadType];\n\n const { allowedExtensions, sizeLimit } = typeConfig;\n\n if (!validateFileExtension(filename, allowedExtensions)) {\n return {\n isValid: false,\n error: `File extension not allowed for ${uploadType.toLowerCase()} files. Allowed extensions: ${allowedExtensions.join(', ')}`,\n };\n }\n\n if (fileSize !== undefined && fileSize > sizeLimit) {\n const maxSizeMB = Math.round(sizeLimit / (1024 * 1024));\n\n return {\n isValid: false,\n error: `File size exceeds limit for ${uploadType.toLowerCase()} files. Maximum size: ${maxSizeMB}MB`,\n };\n }\n\n return { isValid: true };\n}\n\n/**\n * Creates a default upload configuration with predefined settings for different file types.\n * This function provides sensible defaults for image, video, document, and other file types,\n * including allowed extensions and size limits. The configuration can be customized with overrides.\n *\n * @param overrides - Optional configuration overrides to merge with the default configuration.\n * @returns A complete upload configuration object with defaults and any provided overrides.\n */\nexport function createUploadConfig(overrides?: Partial<I_UploadConfig>): I_UploadConfig {\n return { ...DEFAULT_UPLOAD_CONFIG, ...overrides };\n}\n\n/**\n * Uploads a file with comprehensive validation and error handling.\n * This function processes file uploads with the following features:\n * - Input validation for path and file parameters\n * - Configuration validation for all upload types\n * - File validation using getAndValidateFile function\n * - Automatic directory creation\n * - Stream-based file writing\n * - Comprehensive error handling with standardized response codes\n *\n * @param options - Upload configuration including file, path, type, and optional validation config.\n * @returns A promise that resolves to a standardized response with success status, message, file path, and response codes.\n */\nexport async function upload(options: I_UploadOptions): Promise<I_Return<string>> {\n const { path, file, config, type } = options;\n\n if (!path || typeof path !== 'string') {\n return {\n success: false,\n message: 'Invalid path provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (!file || typeof file !== 'object') {\n return {\n success: false,\n message: 'Invalid file provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (config) {\n const requiredTypes = [E_UploadType.IMAGE, E_UploadType.VIDEO, E_UploadType.DOCUMENT, E_UploadType.OTHER];\n\n for (const requiredType of requiredTypes) {\n if (!config[requiredType] || !Array.isArray(config[requiredType].allowedExtensions) || config[requiredType].allowedExtensions.length === 0) {\n return {\n success: false,\n message: `Invalid config for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n if (typeof config[requiredType].sizeLimit !== 'number' || config[requiredType].sizeLimit <= 0) {\n return {\n success: false,\n message: `Invalid size limit for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n }\n }\n\n try {\n const fileData = await getAndValidateFile(type, await file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n const dir = dirname(path);\n\n if (!pathExistsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const writeStream = createReadStream();\n const out = createWriteStream(path);\n writeStream.pipe(out);\n\n await new Promise<void>((resolve, reject) => {\n out.on('finish', () => resolve());\n out.on('error', reject);\n writeStream.on('error', reject);\n });\n\n return {\n success: true,\n result: path,\n message: 'File uploaded successfully',\n code: RESPONSE_STATUS.OK.CODE,\n };\n }\n catch (error) {\n return {\n success: false,\n message: error instanceof Error ? error.message : 'File upload failed',\n code: RESPONSE_STATUS.INTERNAL_SERVER_ERROR.CODE,\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"upload.util.js","names":[],"sources":["../../../src/node/upload/upload.util.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\nimport nodePath from 'node:path';\nimport { Transform } from 'node:stream';\nimport { ReadableStream } from 'node:stream/web';\n\nimport type { I_Return } from '#typescript/index.js';\n\nimport { RESPONSE_STATUS } from '#constant/index.js';\n\nimport type { I_UploadConfig, I_UploadFile, I_UploadFileData, I_UploadOptions, I_UploadTypeConfig, I_UploadValidationConfig } from './upload.type.js';\n\nimport { createWriteStream, mkdirSync, pathExistsSync } from '../fs/index.js';\nimport { dirname } from '../path/index.js';\nimport { BYTES_PER_MB, DEFAULT_UPLOAD_CONFIG } from './upload.constant.js';\nimport { E_UploadType } from './upload.type.js';\n\n/**\n * Calculates the size of a file from a readable stream.\n * This function reads through the entire stream to determine the total byte size\n * by accumulating the length of each data chunk.\n *\n * @param stream - The readable stream to calculate the size for.\n * @returns A promise that resolves to the total size of the stream in bytes.\n */\nexport async function getFileSizeFromStream(stream: NodeJS.ReadableStream): Promise<number> {\n return new Promise((resolve, reject) => {\n let size = 0;\n stream.on('data', (chunk) => {\n size += chunk.length;\n });\n stream.on('end', () => resolve(size));\n stream.on('error', reject);\n });\n}\n\n/**\n * Extracts and validates file data from an upload file.\n * This function processes upload files by:\n * - Extracting file metadata and creating a readable stream\n * - Calculating the file size from the stream\n * - Validating file size and extension against upload configuration\n * - Returning a standardized response with success status and error codes\n * - Providing validated file data for further processing\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing validated file data or error information.\n */\nexport async function getAndValidateFile(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<I_UploadFileData>> {\n const fileData = await (await file).file;\n const stream = fileData.createReadStream();\n // Stream is consumed here for validation; callers use createReadStream() again for the actual write.\n // This is intentional — createReadStream() is a factory that yields a new stream per call.\n const fileSize = await getFileSizeFromStream(stream);\n const uploadConfig = config ?? createUploadConfig();\n\n const validationResult = validateUpload(\n { filename: fileData.filename, fileSize },\n uploadConfig,\n type,\n );\n\n if (!validationResult.isValid) {\n return {\n success: false,\n message: validationResult.error || 'File validation failed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n return {\n success: true,\n result: fileData,\n message: 'File validated successfully',\n };\n}\n\n/**\n * Creates a validated web-readable stream from an upload file with size validation.\n * This function processes file uploads for web environments by:\n * - Validating file data using getAndValidateFile function\n * - Creating a size validation transform stream to monitor upload progress\n * - Returning a web-compatible ReadableStream with real-time validation\n * - Providing standardized error responses for validation failures\n * - Wrapping the stream in a standardized response format\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing either a web ReadableStream or error information.\n */\nexport async function getFileWebStream(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<ReadableStream<Uint8Array>>> {\n const uploadConfig = config ?? createUploadConfig();\n const typeConfig = uploadConfig[type];\n\n const fileData = await getAndValidateFile(type, file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n let remainingBytes = typeConfig.sizeLimit;\n\n const sizeValidationStream = new Transform({\n transform(chunk: Buffer, _enc: BufferEncoding, cb) {\n remainingBytes -= chunk.length;\n\n if (remainingBytes < 0) {\n cb(new Error(`File size exceeds limit of ${typeConfig.sizeLimit / BYTES_PER_MB}MB`));\n }\n else {\n cb(null, chunk);\n }\n },\n });\n const originalStream = createReadStream();\n const validatedStream = originalStream.pipe(sizeValidationStream);\n\n return {\n success: true,\n result: new ReadableStream<Uint8Array>({\n start(controller) {\n validatedStream.on('data', (chunk: Buffer | string) => {\n controller.enqueue(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n });\n validatedStream.on('end', () => controller.close());\n validatedStream.on('error', (err: unknown) => controller.error(err));\n },\n }),\n };\n}\n\n/**\n * Validates if a file has an allowed extension.\n * This function extracts the file extension from the filename and checks if it's\n * included in the list of allowed extensions (case-insensitive comparison).\n *\n * @param filename - The filename to check for valid extension.\n * @param allowedExtensions - An array of allowed file extensions (without dots).\n * @returns True if the file extension is allowed, false otherwise.\n */\nexport function validateFileExtension(filename: string, allowedExtensions: string[]): boolean {\n const lastDotIndex = filename.lastIndexOf('.');\n\n if (lastDotIndex === -1) {\n return false;\n }\n\n const extension = filename.substring(lastDotIndex + 1).toLowerCase();\n\n return allowedExtensions.includes(extension);\n}\n\n/**\n * Validates an upload against the specified configuration.\n * This function performs comprehensive validation including:\n * - File extension validation against allowed extensions\n * - File size validation against size limits\n * - Returns detailed error messages for validation failures\n *\n * @param config - The validation configuration including filename and optional file size.\n * @param uploadConfig - The upload configuration containing allowed extensions and size limits.\n * @param uploadType - The type of upload being validated.\n * @returns An object indicating validation success and optional error message.\n */\nexport function validateUpload(\n config: I_UploadValidationConfig,\n uploadConfig: I_UploadConfig,\n uploadType: E_UploadType,\n): { isValid: boolean; error?: string } {\n const { filename, fileSize } = config;\n const typeConfig: I_UploadTypeConfig = uploadConfig[uploadType];\n\n const { allowedExtensions, sizeLimit } = typeConfig;\n\n if (!validateFileExtension(filename, allowedExtensions)) {\n return {\n isValid: false,\n error: `File extension not allowed for ${uploadType.toLowerCase()} files. Allowed extensions: ${allowedExtensions.join(', ')}`,\n };\n }\n\n if (fileSize !== undefined && fileSize > sizeLimit) {\n const maxSizeMB = Math.round(sizeLimit / (1024 * 1024));\n\n return {\n isValid: false,\n error: `File size exceeds limit for ${uploadType.toLowerCase()} files. Maximum size: ${maxSizeMB}MB`,\n };\n }\n\n return { isValid: true };\n}\n\n/**\n * Creates a default upload configuration with predefined settings for different file types.\n * This function provides sensible defaults for image, video, document, and other file types,\n * including allowed extensions and size limits. The configuration can be customized with overrides.\n *\n * @param overrides - Optional configuration overrides to merge with the default configuration.\n * @returns A complete upload configuration object with defaults and any provided overrides.\n */\nexport function createUploadConfig(overrides?: Partial<I_UploadConfig>): I_UploadConfig {\n return { ...DEFAULT_UPLOAD_CONFIG, ...overrides };\n}\n\n/**\n * Uploads a file with comprehensive validation and error handling.\n * This function processes file uploads with the following features:\n * - Input validation for path and file parameters\n * - Configuration validation for all upload types\n * - File validation using getAndValidateFile function\n * - Automatic directory creation\n * - Stream-based file writing\n * - Comprehensive error handling with standardized response codes\n *\n * @param options - Upload configuration including file, path, type, and optional validation config.\n * @returns A promise that resolves to a standardized response with success status, message, file path, and response codes.\n */\nexport async function upload(options: I_UploadOptions): Promise<I_Return<string>> {\n const { path, file, config, type } = options;\n\n if (!path || typeof path !== 'string') {\n return {\n success: false,\n message: 'Invalid path provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n // Security: Prevent directory traversal attacks by checking the resolved path\n const resolvedPath = nodePath.resolve(path);\n\n if (resolvedPath !== nodePath.normalize(path) && path.includes('..')) {\n return {\n success: false,\n message: 'Path traversal detected: \"..\" segments are not allowed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (!file || typeof file !== 'object') {\n return {\n success: false,\n message: 'Invalid file provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (config) {\n const requiredTypes = [E_UploadType.IMAGE, E_UploadType.VIDEO, E_UploadType.DOCUMENT, E_UploadType.OTHER];\n\n for (const requiredType of requiredTypes) {\n if (!config[requiredType] || !Array.isArray(config[requiredType].allowedExtensions) || config[requiredType].allowedExtensions.length === 0) {\n return {\n success: false,\n message: `Invalid config for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n if (typeof config[requiredType].sizeLimit !== 'number' || config[requiredType].sizeLimit <= 0) {\n return {\n success: false,\n message: `Invalid size limit for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n }\n }\n\n try {\n const fileData = await getAndValidateFile(type, await file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n const dir = dirname(path);\n\n if (!pathExistsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const writeStream = createReadStream();\n const out = createWriteStream(path);\n writeStream.pipe(out);\n\n await new Promise<void>((resolve, reject) => {\n out.on('finish', () => resolve());\n out.on('error', reject);\n writeStream.on('error', reject);\n });\n\n return {\n success: true,\n result: path,\n message: 'File uploaded successfully',\n code: RESPONSE_STATUS.OK.CODE,\n };\n }\n catch (error) {\n return {\n success: false,\n message: error instanceof Error ? error.message : 'File upload failed',\n code: RESPONSE_STATUS.INTERNAL_SERVER_ERROR.CODE,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region node_modules/.pnpm/vitest@4.1.0_@types+node@25.5.0_jsdom@29.0.0_@noble+hashes@1.8.0__vite@8.0.1_@types+nod_36acd00c2670b611b011192023dc5bd9/node_modules/vitest/dist/config.js
|
|
2
|
+
function e(e) {
|
|
3
|
+
return e;
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
export { e as defineConfig };
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","names":[],"sources":["../../../../../../../node_modules/.pnpm/vitest@4.1.0_@types+node@25.5.0_jsdom@29.0.0_@noble+hashes@1.8.0__vite@8.0.
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../../../../../../../node_modules/.pnpm/vitest@4.1.0_@types+node@25.5.0_jsdom@29.0.0_@noble+hashes@1.8.0__vite@8.0.1_@types+nod_36acd00c2670b611b011192023dc5bd9/node_modules/vitest/dist/config.js"],"sourcesContent":["export { c as configDefaults, a as coverageConfigDefaults, d as defaultExclude, b as defaultInclude } from './chunks/defaults.CdU2lD-q.js';\nexport { mergeConfig } from 'vite';\nexport { d as defaultBrowserPort } from './chunks/constants.CPYnjOGj.js';\nimport 'node:os';\nimport './chunks/env.D4Lgay0q.js';\nimport 'std-env';\n\nfunction defineConfig(config) {\n\treturn config;\n}\nfunction defineProject(config) {\n\treturn config;\n}\n\nexport { defineConfig, defineProject };\n"],"x_google_ignoreList":[0],"mappings":";AAOA,SAAS,EAAa,GAAQ;AAC7B,QAAO"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyberskill/shared",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.10.0",
|
|
5
5
|
"description": "CyberSkill Shared",
|
|
6
6
|
"author": "Stephen Cheng",
|
|
7
7
|
"license": "MIT",
|
|
@@ -234,7 +234,7 @@
|
|
|
234
234
|
"@apollo/client-integration-nextjs": "0.14.4",
|
|
235
235
|
"@apollo/server": "5.4.0",
|
|
236
236
|
"@as-integrations/express5": "1.1.2",
|
|
237
|
-
"@dotenvx/dotenvx": "1.
|
|
237
|
+
"@dotenvx/dotenvx": "1.56.0",
|
|
238
238
|
"@eddeee888/gcg-typescript-resolver-files": "0.15.0",
|
|
239
239
|
"@eslint-react/eslint-plugin": "2.13.0",
|
|
240
240
|
"@graphql-codegen/cli": "6.2.1",
|
|
@@ -252,7 +252,7 @@
|
|
|
252
252
|
"cors": "2.8.6",
|
|
253
253
|
"date-fns": "4.1.0",
|
|
254
254
|
"envalid": "8.1.1",
|
|
255
|
-
"eslint-config-next": "16.
|
|
255
|
+
"eslint-config-next": "16.2.0",
|
|
256
256
|
"eslint-plugin-format": "2.0.1",
|
|
257
257
|
"eslint-plugin-react-hooks": "7.0.1",
|
|
258
258
|
"eslint-plugin-react-refresh": "0.5.2",
|
|
@@ -267,7 +267,7 @@
|
|
|
267
267
|
"graphql-upload": "17.0.0",
|
|
268
268
|
"graphql-ws": "6.0.7",
|
|
269
269
|
"helmet": "8.1.0",
|
|
270
|
-
"i18next": "25.8.
|
|
270
|
+
"i18next": "25.8.20",
|
|
271
271
|
"localforage": "1.10.0",
|
|
272
272
|
"migrate-mongo": "14.0.7",
|
|
273
273
|
"mongodb": "7.1.0",
|
|
@@ -288,12 +288,12 @@
|
|
|
288
288
|
"@commitlint/cli": "20.5.0",
|
|
289
289
|
"@commitlint/config-conventional": "20.5.0",
|
|
290
290
|
"@eslint/config-inspector": "1.5.0",
|
|
291
|
-
"@next/eslint-plugin-next": "16.
|
|
291
|
+
"@next/eslint-plugin-next": "16.2.0",
|
|
292
292
|
"@semantic-release/changelog": "6.0.3",
|
|
293
293
|
"@semantic-release/git": "10.0.1",
|
|
294
294
|
"@semantic-release/github": "12.0.6",
|
|
295
|
-
"@storybook/react": "10.3.
|
|
296
|
-
"@storybook/react-vite": "10.3.
|
|
295
|
+
"@storybook/react": "10.3.1",
|
|
296
|
+
"@storybook/react-vite": "10.3.1",
|
|
297
297
|
"@testing-library/dom": "10.4.1",
|
|
298
298
|
"@testing-library/jest-dom": "6.9.1",
|
|
299
299
|
"@testing-library/react": "16.3.2",
|
|
@@ -327,7 +327,7 @@
|
|
|
327
327
|
"simple-git-hooks": "2.13.1",
|
|
328
328
|
"tsx": "4.21.0",
|
|
329
329
|
"typescript": "5.9.3",
|
|
330
|
-
"vite": "8.0.
|
|
330
|
+
"vite": "8.0.1",
|
|
331
331
|
"vite-plugin-dts": "4.5.4",
|
|
332
332
|
"vitest": "4.1.0"
|
|
333
333
|
},
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
//#region node_modules/.pnpm/vitest@4.1.0_@types+node@25.5.0_jsdom@29.0.0_@noble+hashes@1.8.0__vite@8.0.0_@types+nod_53aa4254f295b3c40bb8f17b6ab226b5/node_modules/vitest/dist/config.js
|
|
2
|
-
function e(e) {
|
|
3
|
-
return e;
|
|
4
|
-
}
|
|
5
|
-
//#endregion
|
|
6
|
-
export { e as defineConfig };
|
|
7
|
-
|
|
8
|
-
//# sourceMappingURL=config.js.map
|