offline-cloudinary 2.0.0 → 2.1.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/CHANGELOG.md CHANGED
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1.1] - 2026-01-15
9
+
10
+ ### Fixed
11
+
12
+ - Fixed file path resolution in `viewImage` controller to properly use cached file paths
13
+ - Ensured in-memory cache is explicitly loaded from disk on server startup via `initialise()` call
14
+
15
+ ## [2.1.0] - 2025-12-18
16
+
17
+ ### Added
18
+
19
+ - **In-memory caching system**: File mappings are now cached in memory for dramatically faster operations
20
+ - **Dirty flag tracking**: Efficient cache invalidation system that only syncs to disk when changes occur
21
+ - **Automatic periodic sync**: Background task (500ms intervals) automatically persists cache changes to disk
22
+ - **Graceful shutdown**: Process cleanup handler that ensures all in-memory changes are synced to disk on exit
23
+
24
+ ### Changed
25
+
26
+ - `upload()` and `destroy()` operations now use in-memory cache for improved performance
27
+ - `initialise()` method now handles lazy initialization and cache management
28
+ - Improved resource management with automatic interval cleanup
29
+
30
+ ### Performance
31
+
32
+ - Significantly reduced disk I/O operations
33
+ - Faster file operations through in-memory lookups
34
+ - Minimal overhead with periodic batch writes to disk
35
+
36
+ ### Fully Backward Compatible
37
+
38
+ All v2.0.0 APIs remain unchanged. The caching system is transparent to end users.
39
+
8
40
  ## [2.0.0] - 2025-12-15
9
41
 
10
42
  ### Breaking Changes
@@ -62,7 +94,7 @@ const result = await offlineCloudinary.upload("./photo.jpg");
62
94
  const result = await offlineCloudinary.upload("./photo.jpg");
63
95
  // result.public_id is now a UUID like "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
64
96
  // result.url is "http://localhost:3000/file/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
65
- // result.secure_url is the actual file path
97
+ // result.secure_url is "http://localhost:3000/file/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
66
98
  ```
67
99
 
68
100
  #### 4. Update destroy calls
package/index.js CHANGED
@@ -9,12 +9,18 @@ app.use(cors());
9
9
 
10
10
  app.use("/file", fileRoutes);
11
11
 
12
- const startEmulator = () => {
12
+ const startEmulator = async() => {
13
13
  const portNumber = process.env.CLOUDINARY_OFFLINE_PORT;
14
14
  if (!portNumber)
15
15
  throw new Error("Please set CLOUDINARY_OFFLINE_PORT in your .env file");
16
- app.listen(portNumber, () =>
16
+ await offlineCloudinary.initialise()
17
+ app.listen(portNumber, () =>{
17
18
  console.log("Offline Cloudinary running on port", portNumber)
19
+ process.on("SIGINT", async()=>{
20
+ if (offlineCloudinary.syncActive) clearInterval(offlineCloudinary.syncActive)
21
+ await offlineCloudinary.syncToDisk()
22
+ })
23
+ }
18
24
  );
19
25
  };
20
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offline-cloudinary",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "An offline Cloudinary-like file manager with HTTP server emulator for local uploads, deletions, and testing without internet access.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,10 +1,8 @@
1
1
  import offlineCloudinary from '../utils/offline-cloudinary.js';
2
- import fs from 'fs/promises';
3
2
 
4
3
  export const viewImage = async(req, res)=>{
5
4
  const uploadId = req.params.id
6
- const data = await fs.readFile("uploads.json", "utf-8")
7
- const mappings = JSON.parse(data)
5
+ const mappings = offlineCloudinary.mappingsInMemory
8
6
  res.setHeader("Content-Disposition", "inline")
9
- return res.sendFile(`${offlineCloudinary.rootPath}/${mappings[uploadId]}`)
7
+ return res.sendFile(`${mappings[uploadId]}`)
10
8
  }
@@ -8,6 +8,29 @@ class OfflineCloudinary {
8
8
  throw new Error("Please set CLOUDINARY_OFFLINE_PATH in your .env file");
9
9
  }
10
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;
11
34
  }
12
35
 
13
36
  /**
@@ -17,9 +40,7 @@ class OfflineCloudinary {
17
40
  * @returns Cloudinary-like response
18
41
  */
19
42
  async upload(tempFilePath, options = {}) {
20
- const portNumber = process.env.CLOUDINARY_OFFLINE_PORT;
21
- if (!portNumber)
22
- throw new Error("Please set CLOUDINARY_OFFLINE_PORT in your .env file");
43
+ const portNumber = process.env.CLOUDINARY_OFFLINE_PORT || 3500;
23
44
  await fs.access(tempFilePath).catch(() => {
24
45
  throw new Error(`File not found: ${tempFilePath}`);
25
46
  });
@@ -47,12 +68,8 @@ class OfflineCloudinary {
47
68
 
48
69
  const uploadId = crypto.randomUUID();
49
70
 
50
- const data = await fs.readFile("uploads.json", "utf-8");
51
-
52
- const mappings = JSON.parse(data);
53
- mappings[uploadId] = finalPath;
54
-
55
- await fs.writeFile("uploads.json", JSON.stringify(mappings));
71
+ this.mappingsInMemory[uploadId] = finalPath;
72
+ this.mappingsInMemory.isDirty = true;
56
73
 
57
74
  // Return Cloudinary-like response
58
75
  return {
@@ -84,9 +101,12 @@ class OfflineCloudinary {
84
101
  */
85
102
  async destroy(public_id) {
86
103
  const uploadId = public_id;
87
- const data = await fs.readFile("uploads.json", "utf-8");
88
- const mappings = JSON.parse(data);
89
- await fs.unlink(mappings[uploadId]);
104
+ const filePath = this.mappingsInMemory[uploadId];
105
+ if (filePath) {
106
+ await fs.unlink(filePath);
107
+ delete this.mappingsInMemory[uploadId];
108
+ this.mappingsInMemory.isDirty = true;
109
+ }
90
110
  return { result: "ok" };
91
111
  }
92
112
 
@@ -97,6 +117,7 @@ class OfflineCloudinary {
97
117
  async clearStorage() {
98
118
  await fs.rm(this.rootPath, { recursive: true, force: true });
99
119
  await fs.mkdir(this.rootPath);
120
+ this.mappingsInMemory = { isDirty: false };
100
121
  return { result: "ok" };
101
122
  }
102
123
  }
@@ -1 +0,0 @@
1
- {}