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 +33 -1
- package/index.js +8 -2
- package/package.json +1 -1
- package/src/controllers/fileControllers.js +2 -4
- package/src/utils/offline-cloudinary.js +33 -12
- package/src/utils/uploads.json +0 -1
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
7
|
-
const mappings = JSON.parse(data)
|
|
5
|
+
const mappings = offlineCloudinary.mappingsInMemory
|
|
8
6
|
res.setHeader("Content-Disposition", "inline")
|
|
9
|
-
return res.sendFile(`${
|
|
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
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
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
|
}
|
package/src/utils/uploads.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{}
|