offline-cloudinary 1.0.0 → 2.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,115 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.1.0] - 2025-12-18
9
+
10
+ ### Added
11
+
12
+ - **In-memory caching system**: File mappings are now cached in memory for dramatically faster operations
13
+ - **Dirty flag tracking**: Efficient cache invalidation system that only syncs to disk when changes occur
14
+ - **Automatic periodic sync**: Background task (500ms intervals) automatically persists cache changes to disk
15
+ - **Graceful shutdown**: Process cleanup handler that ensures all in-memory changes are synced to disk on exit
16
+
17
+ ### Changed
18
+
19
+ - `upload()` and `destroy()` operations now use in-memory cache for improved performance
20
+ - `initialise()` method now handles lazy initialization and cache management
21
+ - Improved resource management with automatic interval cleanup
22
+
23
+ ### Performance
24
+
25
+ - Significantly reduced disk I/O operations
26
+ - Faster file operations through in-memory lookups
27
+ - Minimal overhead with periodic batch writes to disk
28
+
29
+ ### Fully Backward Compatible
30
+
31
+ All v2.0.0 APIs remain unchanged. The caching system is transparent to end users.
32
+
33
+ ## [2.0.0] - 2025-12-15
34
+
35
+ ### Breaking Changes
36
+
37
+ - **Main export location changed**: Moved from `src/index.js` to root `index.js`
38
+ - **Port configuration**: Now requires `CLOUDINARY_OFFLINE_PORT` environment variable
39
+ - **Public ID format**: `public_id` is now a UUID instead of a file path
40
+ - **Upload response URL**: The `url` field now returns an HTTP endpoint (`http://localhost:PORT/file/{id}`) instead of a file path
41
+ - **Secure URL**: The `secure_url` field also now returns an HTTP endpoint (`http://localhost:PORT/file/{id}`) instead of a file path
42
+
43
+ ### Added
44
+
45
+ - **HTTP Server Emulator**: New `startEmulator()` function to run a local Express server that serves uploaded files
46
+ - **File viewing endpoint**: GET `/file/:id` endpoint to view/download uploaded files via HTTP
47
+ - **Upload tracking**: Internal `uploads.json` file maps UUIDs to file paths
48
+ - **New environment variable**: `CLOUDINARY_OFFLINE_PORT` for configuring the server port
49
+ - **Express integration**: Built-in CORS and Express routing support
50
+
51
+ ### Changed
52
+
53
+ - File tracking now uses a JSON mapping file (`uploads.json`) instead of direct file paths
54
+ - Upload method now generates UUID-based public IDs for better privacy and consistency
55
+ - Files can now be accessed via HTTP URLs when the emulator is running
56
+
57
+ ### Migration Guide from v1.x to v2.x
58
+
59
+ #### 1. Update your `.env` file
60
+
61
+ Add the new port configuration:
62
+
63
+ ```env
64
+ CLOUDINARY_OFFLINE_PATH=./offline_uploads
65
+ CLOUDINARY_OFFLINE_PORT=3000
66
+ ```
67
+
68
+ #### 2. Start the emulator
69
+
70
+ ```js
71
+ import { startEmulator, offlineCloudinary } from "offline-cloudinary";
72
+
73
+ // Start the HTTP server
74
+ startEmulator();
75
+ ```
76
+
77
+ #### 3. Update public_id handling
78
+
79
+ **Before (v1):**
80
+ ```js
81
+ const result = await offlineCloudinary.upload("./photo.jpg");
82
+ // result.public_id was a file path like "./offline_uploads/photo.jpg"
83
+ ```
84
+
85
+ **After (v2):**
86
+ ```js
87
+ const result = await offlineCloudinary.upload("./photo.jpg");
88
+ // result.public_id is now a UUID like "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
89
+ // result.url is "http://localhost:3000/file/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
90
+ // result.secure_url is the actual file path
91
+ ```
92
+
93
+ #### 4. Update destroy calls
94
+
95
+ Use the UUID from upload response:
96
+
97
+ ```js
98
+ const uploadResult = await offlineCloudinary.upload("./photo.jpg");
99
+ await offlineCloudinary.destroy(uploadResult.public_id); // Pass the UUID
100
+ ```
101
+
102
+ ---
103
+
104
+ ## [1.0.0] - Initial Release
105
+
106
+ ### Added
107
+
108
+ - Initial release with basic upload, delete, and clearStorage functionality
109
+ - Environment-based path configuration via `CLOUDINARY_OFFLINE_PATH`
110
+ - Cloudinary-like response format
111
+ - Support for nested folder structures
112
+ - Custom filename option
113
+
114
+ [2.0.0]: https://github.com/MaxEssien/offline-cloudinary/compare/v1.0.0...v2.0.0
115
+ [1.0.0]: https://github.com/MaxEssien/offline-cloudinary/releases/tag/v1.0.0
package/README.md CHANGED
@@ -6,9 +6,10 @@ An **offline Cloudinary-like file manager** for Node.js — designed for develop
6
6
 
7
7
  ## Features
8
8
 
9
- - Local file uploads and deletions
9
+ - Local file uploads and deletions with HTTP server emulator
10
10
  - Cloudinary-style API responses
11
11
  - Simple environment-based configuration
12
+ - Built-in Express server to serve uploaded files via HTTP
12
13
  - Clear all stored uploads with one command
13
14
  - Perfect for testing, prototyping, or offline development
14
15
 
@@ -30,20 +31,25 @@ yarn add offline-cloudinary
30
31
 
31
32
  ## Setup
32
33
 
33
- In your projects `.env` file, define the base path for all offline uploads:
34
+ In your project's `.env` file, define the configuration for offline uploads:
34
35
 
35
- ```
36
+ ```env
36
37
  CLOUDINARY_OFFLINE_PATH=./offline_uploads
38
+ CLOUDINARY_OFFLINE_PORT=3000
37
39
  ```
38
40
 
39
- This path will serve as your “offline cloud” — all uploaded files will be stored here.
41
+ - **`CLOUDINARY_OFFLINE_PATH`**: Base directory where uploaded files will be stored
42
+ - **`CLOUDINARY_OFFLINE_PORT`**: Port number for the HTTP server emulator
40
43
 
41
44
  ---
42
45
 
43
- ## Usage Example
46
+ ## Quick Start
44
47
 
45
48
  ```js
46
- import offlineCloudinary from "offline-cloudinary";
49
+ import { startEmulator, offlineCloudinary } from "offline-cloudinary";
50
+
51
+ // Start the HTTP server to serve uploaded files
52
+ startEmulator();
47
53
 
48
54
  (async () => {
49
55
  try {
@@ -54,6 +60,11 @@ import offlineCloudinary from "offline-cloudinary";
54
60
  });
55
61
 
56
62
  console.log("Upload result:", uploadResult);
63
+ // uploadResult.url: "http://localhost:3000/file/{uuid}"
64
+ // uploadResult.public_id: UUID identifier
65
+
66
+ // Access the file via HTTP
67
+ console.log(`View file at: ${uploadResult.url}`);
57
68
 
58
69
  // Delete the uploaded file
59
70
  const deleteResult = await offlineCloudinary.destroy(uploadResult.public_id);
@@ -72,16 +83,39 @@ import offlineCloudinary from "offline-cloudinary";
72
83
 
73
84
  ## API Reference
74
85
 
75
- ### **`new OfflineCloudinary()`**
76
- Automatically instantiated when imported.
86
+ ### **`startEmulator()`**
87
+
88
+ Starts the Express HTTP server to serve uploaded files.
89
+
90
+ **Important**: Must be called to enable HTTP access to uploaded files via the `url` field.
91
+
92
+ **Environment Variables Required:**
93
+ - `CLOUDINARY_OFFLINE_PORT` - Port number for the server
94
+
95
+ **Example:**
96
+ ```js
97
+ import { startEmulator } from "offline-cloudinary";
98
+
99
+ startEmulator();
100
+ // Server running on port specified in CLOUDINARY_OFFLINE_PORT
101
+ ```
102
+
103
+ ---
104
+
105
+ ### **`offlineCloudinary`**
106
+
107
+ The main instance for file operations. Automatically instantiated when imported.
77
108
  Throws an error if `CLOUDINARY_OFFLINE_PATH` is not set.
78
109
 
79
110
  ---
80
111
 
81
- ### **`upload(tempFilePath, options)`**
112
+ ### **`offlineCloudinary.upload(tempFilePath, options)`**
82
113
 
83
114
  Uploads a file from a temporary path to your offline storage.
84
115
 
116
+ **Environment Variables Required:**
117
+ - `CLOUDINARY_OFFLINE_PORT` - Used to generate the HTTP URL
118
+
85
119
  **Parameters**
86
120
  | Name | Type | Description |
87
121
  |------|------|--------------|
@@ -90,33 +124,67 @@ Uploads a file from a temporary path to your offline storage.
90
124
  | `options.fileName` | `string` | Optional custom file name (without extension) |
91
125
 
92
126
  **Returns**
127
+
93
128
  A Cloudinary-like object containing:
94
129
  ```js
95
130
  {
96
- asset_id, public_id, version, format, resource_type,
97
- created_at, bytes, url, secure_url, ...
131
+ asset_id: "uuid-v4",
132
+ public_id: "uuid-v4", // UUID identifier for this file
133
+ version: 1702654321000, // Timestamp
134
+ version_id: "uuid-v4",
135
+ signature: "hex-string",
136
+ width: null,
137
+ height: null,
138
+ format: "jpg", // File extension
139
+ resource_type: "image",
140
+ created_at: "2025-12-15T10:30:00.000Z",
141
+ tags: [],
142
+ pages: 1,
143
+ bytes: 102400, // File size in bytes
144
+ type: "upload",
145
+ etag: "hex-string",
146
+ placeholder: false,
147
+ url: "http://localhost:3000/file/{uuid}", // HTTP URL to access file
148
+ secure_url: "/path/to/actual/file.jpg" // Actual file system path
98
149
  }
99
150
  ```
100
151
 
152
+ **Example:**
153
+ ```js
154
+ const result = await offlineCloudinary.upload("./photo.jpg", {
155
+ folder: "users/avatars",
156
+ fileName: "profile-pic"
157
+ });
158
+
159
+ console.log(result.url); // "http://localhost:3000/file/abc123..."
160
+ console.log(result.public_id); // "abc123-def456-789..."
161
+ ```
162
+
101
163
  ---
102
164
 
103
- ### **`destroy(public_id)`**
165
+ ### **`offlineCloudinary.destroy(public_id)`**
104
166
 
105
- Deletes a file from your offline storage.
167
+ Deletes a file from your offline storage using its UUID.
106
168
 
107
169
  **Parameters**
108
170
  | Name | Type | Description |
109
171
  |------|------|--------------|
110
- | `public_id` | `string` | Full path returned by the upload method |
172
+ | `public_id` | `string` | UUID returned by the upload method |
111
173
 
112
174
  **Returns**
113
175
  ```js
114
176
  { result: "ok" }
115
177
  ```
116
178
 
179
+ **Example:**
180
+ ```js
181
+ const uploadResult = await offlineCloudinary.upload("./photo.jpg");
182
+ await offlineCloudinary.destroy(uploadResult.public_id);
183
+ ```
184
+
117
185
  ---
118
186
 
119
- ### **`clearStorage()`**
187
+ ### **`offlineCloudinary.clearStorage()`**
120
188
 
121
189
  Deletes all files and folders in your offline Cloudinary storage.
122
190
 
@@ -125,12 +193,19 @@ Deletes all files and folders in your offline Cloudinary storage.
125
193
  { result: "ok" }
126
194
  ```
127
195
 
196
+ **Example:**
197
+ ```js
198
+ await offlineCloudinary.clearStorage();
199
+ console.log("All files deleted");
200
+ ```
201
+
128
202
  ---
129
203
 
130
204
  ## Example .env
131
205
 
132
- ```
206
+ ```env
133
207
  CLOUDINARY_OFFLINE_PATH=./uploads
208
+ CLOUDINARY_OFFLINE_PORT=3000
134
209
  ```
135
210
 
136
211
  ---
@@ -140,6 +215,7 @@ CLOUDINARY_OFFLINE_PATH=./uploads
140
215
  ```
141
216
  project/
142
217
  ├── .env
218
+ ├── uploads.json # Auto-generated UUID mapping
143
219
  ├── uploads/
144
220
  │ └── users/
145
221
  │ └── avatars/
@@ -149,6 +225,42 @@ project/
149
225
 
150
226
  ---
151
227
 
228
+ ## Migration from v1.x to v2.x
229
+
230
+ ### Breaking Changes
231
+
232
+ 1. **New environment variable required**: `CLOUDINARY_OFFLINE_PORT`
233
+ 2. **Import changed**: Now exports `{ startEmulator, offlineCloudinary }` instead of default export
234
+ 3. **public_id format**: Now returns UUID instead of file path
235
+ 4. **URL field**: Now returns HTTP endpoint instead of file path
236
+ 5. **Must call startEmulator()** to serve files via HTTP
237
+
238
+ ### Migration Steps
239
+
240
+ **Before (v1.x):**
241
+ ```js
242
+ import offlineCloudinary from "offline-cloudinary";
243
+
244
+ const result = await offlineCloudinary.upload("./photo.jpg");
245
+ // result.public_id was a file path
246
+ ```
247
+
248
+ **After (v2.x):**
249
+ ```js
250
+ import { startEmulator, offlineCloudinary } from "offline-cloudinary";
251
+
252
+ // Add CLOUDINARY_OFFLINE_PORT=3000 to your .env
253
+
254
+ startEmulator(); // Start the HTTP server
255
+
256
+ const result = await offlineCloudinary.upload("./photo.jpg");
257
+ // result.public_id is now a UUID
258
+ // result.url is "http://localhost:3000/file/{uuid}"
259
+ // result.secure_url is "http://localhost:3000/file/{uuid}"
260
+ ```
261
+
262
+ ---
263
+
152
264
  ## Author
153
265
 
154
266
  **Max Essien**
@@ -162,5 +274,12 @@ project/
162
274
  This project is licensed under the **MIT License** — free for personal and commercial use.
163
275
 
164
276
  ---
277
+
278
+ ## Changelog
279
+
280
+ See [CHANGELOG.md](./CHANGELOG.md) for version history and migration guides.
281
+
282
+ ---
283
+
165
284
  **Offline Cloudinary** — your Cloudinary, anywhere, even offline.
166
285
 
package/index.js ADDED
@@ -0,0 +1,26 @@
1
+ import express from "express";
2
+ import fileRoutes from "./src/routes/fileRoutes.js";
3
+ import offlineCloudinary from "./src/utils/offline-cloudinary";
4
+ import cors from "cors";
5
+
6
+ const app = express();
7
+
8
+ app.use(cors());
9
+
10
+ app.use("/file", fileRoutes);
11
+
12
+ const startEmulator = () => {
13
+ const portNumber = process.env.CLOUDINARY_OFFLINE_PORT;
14
+ if (!portNumber)
15
+ throw new Error("Please set CLOUDINARY_OFFLINE_PORT in your .env file");
16
+ app.listen(portNumber, () =>{
17
+ console.log("Offline Cloudinary running on port", portNumber)
18
+ process.on("SIGINT", async()=>{
19
+ if (offlineCloudinary.syncActive) clearInterval(offlineCloudinary.syncActive)
20
+ await offlineCloudinary.syncToDisk()
21
+ })
22
+ }
23
+ );
24
+ };
25
+
26
+ export { startEmulator, offlineCloudinary };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "offline-cloudinary",
3
- "version": "1.0.0",
4
- "description": "An offline Cloudinary-like file manager for local uploads and deletions.",
5
- "main": "src/utils/OfflineCloudinary.js",
3
+ "version": "2.1.0",
4
+ "description": "An offline Cloudinary-like file manager with HTTP server emulator for local uploads, deletions, and testing without internet access.",
5
+ "main": "index.js",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "cloudinary",
@@ -12,7 +12,12 @@
12
12
  "mock",
13
13
  "testing",
14
14
  "file-system",
15
- "nodejs"
15
+ "nodejs",
16
+ "express",
17
+ "server",
18
+ "emulator",
19
+ "http",
20
+ "file-upload"
16
21
  ],
17
22
  "author": "Max Essien",
18
23
  "license": "MIT",
@@ -20,12 +25,25 @@
20
25
  "type": "git",
21
26
  "url": "https://github.com/MaxEssien/offline-cloudinary"
22
27
  },
23
- "files": ["src"],
28
+ "files": [
29
+ "src",
30
+ "index.js",
31
+ "README.md",
32
+ "CHANGELOG.md",
33
+ "LICENSE"
34
+ ],
24
35
  "scripts": {
25
- "test": "node src/utils/OfflineCloudinary.js"
36
+ "test": "echo \"Error: no test specified\" && exit 1"
37
+ },
38
+ "engines": {
39
+ "node": ">=18.0.0"
26
40
  },
27
41
  "bugs": {
28
42
  "url": "https://github.com/MaxEssien/offline-cloudinary/issues"
29
43
  },
30
- "homepage": "https://github.com/MaxEssien/offline-cloudinary#readme"
44
+ "homepage": "https://github.com/MaxEssien/offline-cloudinary#readme",
45
+ "dependencies": {
46
+ "cors": "^2.8.5",
47
+ "express": "^5.2.1"
48
+ }
31
49
  }
@@ -0,0 +1,10 @@
1
+ import offlineCloudinary from '../utils/offline-cloudinary.js';
2
+ import fs from 'fs/promises';
3
+
4
+ export const viewImage = async(req, res)=>{
5
+ const uploadId = req.params.id
6
+ const data = await fs.readFile("uploads.json", "utf-8")
7
+ const mappings = JSON.parse(data)
8
+ res.setHeader("Content-Disposition", "inline")
9
+ return res.sendFile(`${offlineCloudinary.rootPath}/${mappings[uploadId]}`)
10
+ }
@@ -0,0 +1,8 @@
1
+ import express from "express";
2
+ import { viewImage } from "../controllers/fileControllers.js";
3
+
4
+ const router = express.Router()
5
+
6
+ router.get("/:id", viewImage)
7
+
8
+ export default router
@@ -0,0 +1,129 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+
5
+ class OfflineCloudinary {
6
+ constructor() {
7
+ if (!process.env.CLOUDINARY_OFFLINE_PATH) {
8
+ throw new Error("Please set CLOUDINARY_OFFLINE_PATH in your .env file");
9
+ }
10
+ this.rootPath = process.env.CLOUDINARY_OFFLINE_PATH;
11
+ this.initialised = false;
12
+ this.mappingsInMemory = { isDirty: false };
13
+ this.syncActive = false;
14
+ }
15
+
16
+ async initialise() {
17
+ if (this.initialised) return;
18
+ const filePath = path.join(this.rootPath, "uploads.json");
19
+ await fs.access(filePath).catch(() => fs.writeFile(filePath, "{}"));
20
+ const data = await fs.readFile(filePath, "utf-8");
21
+ this.mappingsInMemory = { ...JSON.parse(data), isDirty: false };
22
+ this.initialised = true;
23
+ this.syncActive = setInterval(() => this.syncToDisk(), 500);
24
+ }
25
+
26
+ async syncToDisk() {
27
+ if (!this.mappingsInMemory.isDirty) return;
28
+ const mappingsCopy = { ...this.mappingsInMemory, isDirty: false };
29
+ await fs.writeFile(
30
+ path.join(this.rootPath, "uploads.json"),
31
+ JSON.stringify(mappingsCopy)
32
+ );
33
+ this.mappingsInMemory.isDirty = false;
34
+ }
35
+
36
+ /**
37
+ * Upload a file
38
+ * @param {string} tempFilePath - Path to the temporary file
39
+ * @param {object} options - { folder: 'nested/folder/path' }
40
+ * @returns Cloudinary-like response
41
+ */
42
+ async upload(tempFilePath, options = {}) {
43
+ await this.initialise();
44
+ const portNumber = process.env.CLOUDINARY_OFFLINE_PORT || 3500;
45
+ await fs.access(tempFilePath).catch(() => {
46
+ throw new Error(`File not found: ${tempFilePath}`);
47
+ });
48
+ const folder = options.folder || "";
49
+ const name = options?.fileName || crypto.randomUUID();
50
+ const fullFolderPath = path.join(this.rootPath, folder);
51
+
52
+ // Ensure folder exists
53
+ await fs.mkdir(fullFolderPath, { recursive: true });
54
+
55
+ // Generate unique filename
56
+ const ext = path.extname(tempFilePath);
57
+ if (!ext?.trim()) throw new Error("Unsupported file type");
58
+ const fileName = name + ext;
59
+
60
+ const finalPath = path.join(fullFolderPath, fileName);
61
+
62
+ // Copy file from temp path
63
+ await fs.copyFile(tempFilePath, finalPath);
64
+
65
+ // Get file stats
66
+ const stats = await fs.stat(finalPath);
67
+
68
+ const now = new Date().toISOString();
69
+
70
+ const uploadId = crypto.randomUUID();
71
+
72
+ this.mappingsInMemory[uploadId] = finalPath;
73
+ this.mappingsInMemory.isDirty = true;
74
+
75
+ // Return Cloudinary-like response
76
+ return {
77
+ asset_id: crypto.randomUUID(),
78
+ public_id: uploadId,
79
+ version: Date.now(),
80
+ version_id: crypto.randomUUID(),
81
+ signature: crypto.randomBytes(16).toString("hex"),
82
+ width: null,
83
+ height: null,
84
+ format: ext.replace(".", ""),
85
+ resource_type: "image",
86
+ created_at: now,
87
+ tags: [],
88
+ pages: 1,
89
+ bytes: stats.size,
90
+ type: "upload",
91
+ etag: crypto.randomBytes(8).toString("hex"),
92
+ placeholder: false,
93
+ url: `http://localhost:${portNumber}/file/${uploadId}`,
94
+ secure_url: `http://localhost:${portNumber}/file/${uploadId}`,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Destroy a file by public_id
100
+ * @param {string} public_id
101
+ * @returns {object} { result: "ok" } if deleted or { result: "not found" }
102
+ */
103
+ async destroy(public_id) {
104
+ await this.initialise();
105
+ const uploadId = public_id;
106
+ const filePath = this.mappingsInMemory[uploadId];
107
+ if (filePath) {
108
+ await fs.unlink(filePath);
109
+ delete this.mappingsInMemory[uploadId];
110
+ this.mappingsInMemory.isDirty = true;
111
+ }
112
+ return { result: "ok" };
113
+ }
114
+
115
+ /**
116
+ * Destroy every files and folder in the local offline cloudinary storage
117
+ * @returns {object} {result: ok} if successful
118
+ */
119
+ async clearStorage() {
120
+ await fs.rm(this.rootPath, { recursive: true, force: true });
121
+ await fs.mkdir(this.rootPath);
122
+ this.mappingsInMemory = { isDirty: false };
123
+ return { result: "ok" };
124
+ }
125
+ }
126
+
127
+ const offlineCloudinary = new OfflineCloudinary();
128
+
129
+ export default offlineCloudinary;
@@ -0,0 +1 @@
1
+ {}
package/src/index.js DELETED
@@ -1,92 +0,0 @@
1
- import fs from "fs/promises";
2
- import path from "path";
3
- import crypto from "crypto";
4
-
5
- class OfflineCloudinary {
6
- constructor() {
7
- if (!process.env.CLOUDINARY_OFFLINE_PATH) {
8
- throw new Error(
9
- "Please set CLOUDINARY_OFFLINE_PATH in your .env file"
10
- );
11
- }
12
- this.rootPath = process.env.CLOUDINARY_OFFLINE_PATH;
13
- }
14
-
15
- /**
16
- * Upload a file
17
- * @param {string} tempFilePath - Path to the temporary file
18
- * @param {object} options - { folder: 'nested/folder/path' }
19
- * @returns Cloudinary-like response
20
- */
21
- async upload(tempFilePath, options = {}) {
22
- await fs.access(tempFilePath).catch(()=>{throw new Error(`File not found: ${tempFilePath}`)})
23
- const folder = options.folder || "";
24
- const name = options?.fileName || crypto.randomUUID();
25
- const fullFolderPath = path.join(this.rootPath, folder);
26
-
27
- // Ensure folder exists
28
- await fs.mkdir(fullFolderPath, { recursive: true });
29
-
30
- // Generate unique filename
31
- const ext = path.extname(tempFilePath);
32
- if (!ext?.trim()) throw new Error("Unsupported file type")
33
- const fileName = name + ext;
34
-
35
- const finalPath = path.join(fullFolderPath, fileName);
36
-
37
- // Copy file from temp path
38
- await fs.copyFile(tempFilePath, finalPath);
39
-
40
- // Get file stats
41
- const stats = await fs.stat(finalPath);
42
-
43
- const now = new Date().toISOString();
44
-
45
- // Return Cloudinary-like response
46
- return {
47
- asset_id: crypto.randomUUID(),
48
- public_id: finalPath,
49
- version: Date.now(),
50
- version_id: crypto.randomUUID(),
51
- signature: crypto.randomBytes(16).toString("hex"),
52
- width: null,
53
- height: null,
54
- format: ext.replace(".", ""),
55
- resource_type: "image",
56
- created_at: now,
57
- tags: [],
58
- pages: 1,
59
- bytes: stats.size,
60
- type: "upload",
61
- etag: crypto.randomBytes(8).toString("hex"),
62
- placeholder: false,
63
- url: finalPath,
64
- secure_url: finalPath,
65
- };
66
- }
67
-
68
- /**
69
- * Destroy a file by public_id
70
- * @param {string} public_id
71
- * @returns {object} { result: "ok" } if deleted or { result: "not found" }
72
- */
73
- async destroy(public_id) {
74
- const filePath = public_id;
75
- await fs.unlink(filePath);
76
- return { result: "ok" };
77
- }
78
-
79
- /**
80
- * Destroy every files and folder in the local offline cloudinary storage
81
- * @returns {object} {result: ok} if successful
82
- */
83
- async clearStorage(){
84
- await fs.rm(this.rootPath, {recursive: true, force: true})
85
- await fs.mkdir(this.rootPath)
86
- return {result: "ok"}
87
- }
88
- }
89
-
90
- const offlineCloudinary = new OfflineCloudinary()
91
-
92
- export default offlineCloudinary;