offline-cloudinary 1.0.0 → 2.0.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,90 @@
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.0.0] - 2025-12-15
9
+
10
+ ### Breaking Changes
11
+
12
+ - **Main export location changed**: Moved from `src/index.js` to root `index.js`
13
+ - **Port configuration**: Now requires `CLOUDINARY_OFFLINE_PORT` environment variable
14
+ - **Public ID format**: `public_id` is now a UUID instead of a file path
15
+ - **Upload response URL**: The `url` field now returns an HTTP endpoint (`http://localhost:PORT/file/{id}`) instead of a file path
16
+ - **Secure URL**: The `secure_url` field also now returns an HTTP endpoint (`http://localhost:PORT/file/{id}`) instead of a file path
17
+
18
+ ### Added
19
+
20
+ - **HTTP Server Emulator**: New `startEmulator()` function to run a local Express server that serves uploaded files
21
+ - **File viewing endpoint**: GET `/file/:id` endpoint to view/download uploaded files via HTTP
22
+ - **Upload tracking**: Internal `uploads.json` file maps UUIDs to file paths
23
+ - **New environment variable**: `CLOUDINARY_OFFLINE_PORT` for configuring the server port
24
+ - **Express integration**: Built-in CORS and Express routing support
25
+
26
+ ### Changed
27
+
28
+ - File tracking now uses a JSON mapping file (`uploads.json`) instead of direct file paths
29
+ - Upload method now generates UUID-based public IDs for better privacy and consistency
30
+ - Files can now be accessed via HTTP URLs when the emulator is running
31
+
32
+ ### Migration Guide from v1.x to v2.x
33
+
34
+ #### 1. Update your `.env` file
35
+
36
+ Add the new port configuration:
37
+
38
+ ```env
39
+ CLOUDINARY_OFFLINE_PATH=./offline_uploads
40
+ CLOUDINARY_OFFLINE_PORT=3000
41
+ ```
42
+
43
+ #### 2. Start the emulator
44
+
45
+ ```js
46
+ import { startEmulator, offlineCloudinary } from "offline-cloudinary";
47
+
48
+ // Start the HTTP server
49
+ startEmulator();
50
+ ```
51
+
52
+ #### 3. Update public_id handling
53
+
54
+ **Before (v1):**
55
+ ```js
56
+ const result = await offlineCloudinary.upload("./photo.jpg");
57
+ // result.public_id was a file path like "./offline_uploads/photo.jpg"
58
+ ```
59
+
60
+ **After (v2):**
61
+ ```js
62
+ const result = await offlineCloudinary.upload("./photo.jpg");
63
+ // result.public_id is now a UUID like "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
64
+ // result.url is "http://localhost:3000/file/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
65
+ // result.secure_url is the actual file path
66
+ ```
67
+
68
+ #### 4. Update destroy calls
69
+
70
+ Use the UUID from upload response:
71
+
72
+ ```js
73
+ const uploadResult = await offlineCloudinary.upload("./photo.jpg");
74
+ await offlineCloudinary.destroy(uploadResult.public_id); // Pass the UUID
75
+ ```
76
+
77
+ ---
78
+
79
+ ## [1.0.0] - Initial Release
80
+
81
+ ### Added
82
+
83
+ - Initial release with basic upload, delete, and clearStorage functionality
84
+ - Environment-based path configuration via `CLOUDINARY_OFFLINE_PATH`
85
+ - Cloudinary-like response format
86
+ - Support for nested folder structures
87
+ - Custom filename option
88
+
89
+ [2.0.0]: https://github.com/MaxEssien/offline-cloudinary/compare/v1.0.0...v2.0.0
90
+ [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,21 @@
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
+ );
19
+ };
20
+
21
+ 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.0.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
@@ -5,9 +5,7 @@ import crypto from "crypto";
5
5
  class OfflineCloudinary {
6
6
  constructor() {
7
7
  if (!process.env.CLOUDINARY_OFFLINE_PATH) {
8
- throw new Error(
9
- "Please set CLOUDINARY_OFFLINE_PATH in your .env file"
10
- );
8
+ throw new Error("Please set CLOUDINARY_OFFLINE_PATH in your .env file");
11
9
  }
12
10
  this.rootPath = process.env.CLOUDINARY_OFFLINE_PATH;
13
11
  }
@@ -19,7 +17,12 @@ class OfflineCloudinary {
19
17
  * @returns Cloudinary-like response
20
18
  */
21
19
  async upload(tempFilePath, options = {}) {
22
- await fs.access(tempFilePath).catch(()=>{throw new Error(`File not found: ${tempFilePath}`)})
20
+ const portNumber = process.env.CLOUDINARY_OFFLINE_PORT;
21
+ if (!portNumber)
22
+ throw new Error("Please set CLOUDINARY_OFFLINE_PORT in your .env file");
23
+ await fs.access(tempFilePath).catch(() => {
24
+ throw new Error(`File not found: ${tempFilePath}`);
25
+ });
23
26
  const folder = options.folder || "";
24
27
  const name = options?.fileName || crypto.randomUUID();
25
28
  const fullFolderPath = path.join(this.rootPath, folder);
@@ -29,7 +32,7 @@ class OfflineCloudinary {
29
32
 
30
33
  // Generate unique filename
31
34
  const ext = path.extname(tempFilePath);
32
- if (!ext?.trim()) throw new Error("Unsupported file type")
35
+ if (!ext?.trim()) throw new Error("Unsupported file type");
33
36
  const fileName = name + ext;
34
37
 
35
38
  const finalPath = path.join(fullFolderPath, fileName);
@@ -42,10 +45,19 @@ class OfflineCloudinary {
42
45
 
43
46
  const now = new Date().toISOString();
44
47
 
48
+ const uploadId = crypto.randomUUID();
49
+
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));
56
+
45
57
  // Return Cloudinary-like response
46
58
  return {
47
59
  asset_id: crypto.randomUUID(),
48
- public_id: finalPath,
60
+ public_id: uploadId,
49
61
  version: Date.now(),
50
62
  version_id: crypto.randomUUID(),
51
63
  signature: crypto.randomBytes(16).toString("hex"),
@@ -60,8 +72,8 @@ class OfflineCloudinary {
60
72
  type: "upload",
61
73
  etag: crypto.randomBytes(8).toString("hex"),
62
74
  placeholder: false,
63
- url: finalPath,
64
- secure_url: finalPath,
75
+ url: `http://localhost:${portNumber}/file/${uploadId}`,
76
+ secure_url: `http://localhost:${portNumber}/file/${uploadId}`,
65
77
  };
66
78
  }
67
79
 
@@ -71,8 +83,10 @@ class OfflineCloudinary {
71
83
  * @returns {object} { result: "ok" } if deleted or { result: "not found" }
72
84
  */
73
85
  async destroy(public_id) {
74
- const filePath = public_id;
75
- await fs.unlink(filePath);
86
+ 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]);
76
90
  return { result: "ok" };
77
91
  }
78
92
 
@@ -80,13 +94,13 @@ class OfflineCloudinary {
80
94
  * Destroy every files and folder in the local offline cloudinary storage
81
95
  * @returns {object} {result: ok} if successful
82
96
  */
83
- async clearStorage(){
84
- await fs.rm(this.rootPath, {recursive: true, force: true})
85
- await fs.mkdir(this.rootPath)
86
- return {result: "ok"}
97
+ async clearStorage() {
98
+ await fs.rm(this.rootPath, { recursive: true, force: true });
99
+ await fs.mkdir(this.rootPath);
100
+ return { result: "ok" };
87
101
  }
88
102
  }
89
103
 
90
- const offlineCloudinary = new OfflineCloudinary()
104
+ const offlineCloudinary = new OfflineCloudinary();
91
105
 
92
- export default offlineCloudinary;
106
+ export default offlineCloudinary;
@@ -0,0 +1 @@
1
+ {}