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 +115 -0
- package/README.md +135 -16
- package/index.js +26 -0
- package/package.json +25 -7
- package/src/controllers/fileControllers.js +10 -0
- package/src/routes/fileRoutes.js +8 -0
- package/src/utils/offline-cloudinary.js +129 -0
- package/src/utils/uploads.json +1 -0
- package/src/index.js +0 -92
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 project
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
### **`
|
|
76
|
-
|
|
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
|
|
97
|
-
|
|
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` |
|
|
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
|
|
4
|
-
"description": "An offline Cloudinary-like file manager for local uploads and
|
|
5
|
-
"main": "
|
|
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": [
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"index.js",
|
|
31
|
+
"README.md",
|
|
32
|
+
"CHANGELOG.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
24
35
|
"scripts": {
|
|
25
|
-
"test": "
|
|
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,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;
|