@sanvika/cloudinary 0.1.2 → 0.1.4
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/dist/client.js +139 -0
- package/dist/index.js +115 -76
- package/package.json +9 -4
package/dist/client.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/SanvikaCloudinaryProvider.jsx
|
|
4
|
+
import { createContext, useContext, useMemo } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
var SanvikaCloudinaryContext = createContext(null);
|
|
7
|
+
function SanvikaCloudinaryProvider({ appName, cloudName, uploadPreset = "ml_default", children }) {
|
|
8
|
+
const value = useMemo(
|
|
9
|
+
() => ({ appName, cloudName, uploadPreset }),
|
|
10
|
+
[appName, cloudName, uploadPreset]
|
|
11
|
+
);
|
|
12
|
+
return /* @__PURE__ */ jsx(SanvikaCloudinaryContext.Provider, { value, children });
|
|
13
|
+
}
|
|
14
|
+
function useSanvikaCloudinary() {
|
|
15
|
+
const ctx = useContext(SanvikaCloudinaryContext);
|
|
16
|
+
if (!ctx) {
|
|
17
|
+
throw new Error("useSanvikaCloudinary must be used within a SanvikaCloudinaryProvider");
|
|
18
|
+
}
|
|
19
|
+
return ctx;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/useCloudinaryUpload.js
|
|
23
|
+
import { useState, useCallback } from "react";
|
|
24
|
+
function useCloudinaryUpload(options = {}) {
|
|
25
|
+
const { folder, resourceType = "image", onProgress } = options;
|
|
26
|
+
const { appName, cloudName, uploadPreset } = useSanvikaCloudinary();
|
|
27
|
+
const [uploading, setUploading] = useState(false);
|
|
28
|
+
const [error, setError] = useState(null);
|
|
29
|
+
const reset = useCallback(() => {
|
|
30
|
+
setError(null);
|
|
31
|
+
setUploading(false);
|
|
32
|
+
}, []);
|
|
33
|
+
const upload = useCallback(
|
|
34
|
+
async (file) => {
|
|
35
|
+
setError(null);
|
|
36
|
+
setUploading(true);
|
|
37
|
+
try {
|
|
38
|
+
const folderPath = folder ? `${appName}/${folder}` : appName;
|
|
39
|
+
const formData = new FormData();
|
|
40
|
+
formData.append("file", file);
|
|
41
|
+
formData.append("upload_preset", uploadPreset);
|
|
42
|
+
formData.append("folder", folderPath);
|
|
43
|
+
const xhr = new XMLHttpRequest();
|
|
44
|
+
const url = `https://api.cloudinary.com/v1_1/${cloudName}/${resourceType}/upload`;
|
|
45
|
+
const result = await new Promise((resolve, reject) => {
|
|
46
|
+
xhr.open("POST", url);
|
|
47
|
+
if (onProgress) {
|
|
48
|
+
xhr.upload.onprogress = (e) => {
|
|
49
|
+
if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
xhr.onload = () => {
|
|
53
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
54
|
+
resolve(JSON.parse(xhr.responseText));
|
|
55
|
+
} else {
|
|
56
|
+
reject(new Error(xhr.responseText || "Upload failed"));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
xhr.onerror = () => reject(new Error("Network error during upload"));
|
|
60
|
+
xhr.send(formData);
|
|
61
|
+
});
|
|
62
|
+
return result;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const msg = (err == null ? void 0 : err.message) || "Upload failed";
|
|
65
|
+
setError(msg);
|
|
66
|
+
throw err;
|
|
67
|
+
} finally {
|
|
68
|
+
setUploading(false);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[appName, cloudName, uploadPreset, folder, resourceType, onProgress]
|
|
72
|
+
);
|
|
73
|
+
return { upload, uploading, error, reset };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/cloudinaryUtils.js
|
|
77
|
+
function isCloudinaryUrl(url) {
|
|
78
|
+
return typeof url === "string" && (url.includes("res.cloudinary.com") || url.includes("cloudinary.com"));
|
|
79
|
+
}
|
|
80
|
+
function extractPublicId(url) {
|
|
81
|
+
if (!isCloudinaryUrl(url)) return null;
|
|
82
|
+
try {
|
|
83
|
+
const parts = url.split("/");
|
|
84
|
+
const uploadIdx = parts.indexOf("upload");
|
|
85
|
+
if (uploadIdx === -1) return null;
|
|
86
|
+
let publicIdPath = parts.slice(uploadIdx + 1).join("/");
|
|
87
|
+
const versionMatch = publicIdPath.match(/^v\d+\//);
|
|
88
|
+
if (versionMatch) {
|
|
89
|
+
publicIdPath = publicIdPath.substring(versionMatch[0].length);
|
|
90
|
+
}
|
|
91
|
+
const lastDot = publicIdPath.lastIndexOf(".");
|
|
92
|
+
if (lastDot !== -1) {
|
|
93
|
+
const ext = publicIdPath.substring(lastDot + 1);
|
|
94
|
+
if (/^[a-zA-Z0-9]{2,5}$/.test(ext)) {
|
|
95
|
+
publicIdPath = publicIdPath.substring(0, lastDot);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return publicIdPath || null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function validatePublicId(publicId) {
|
|
104
|
+
if (!publicId || typeof publicId !== "string") return false;
|
|
105
|
+
const parts = publicId.split("/");
|
|
106
|
+
return parts.length >= 2 && parts.every((p) => p.length > 0);
|
|
107
|
+
}
|
|
108
|
+
function getFolderPath(appName, subfolder) {
|
|
109
|
+
if (!appName) throw new Error("appName is required for getFolderPath");
|
|
110
|
+
const parts = [appName];
|
|
111
|
+
if (subfolder) {
|
|
112
|
+
const cleaned = subfolder.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean).join("/");
|
|
113
|
+
if (cleaned) parts.push(cleaned);
|
|
114
|
+
}
|
|
115
|
+
return parts.join("/");
|
|
116
|
+
}
|
|
117
|
+
function getOptimizedUrl(cloudName, publicId, transforms = {}, resourceType = "image") {
|
|
118
|
+
const base = `https://res.cloudinary.com/${cloudName}/${resourceType}/upload`;
|
|
119
|
+
const parts = Object.entries(transforms).map(([k, v]) => `${k}_${v}`).join(",");
|
|
120
|
+
return parts ? `${base}/${parts}/${publicId}` : `${base}/${publicId}`;
|
|
121
|
+
}
|
|
122
|
+
var TRANSFORM_PRESETS = {
|
|
123
|
+
thumbnail: { w: 150, h: 150, c: "fill", g: "auto", q: "auto", f: "auto" },
|
|
124
|
+
adCard: { w: 300, h: 200, c: "fill", g: "auto", q: "auto", f: "auto" },
|
|
125
|
+
profilePicture: { w: 100, h: 100, c: "fill", g: "face", r: "max", q: "auto", f: "auto" },
|
|
126
|
+
adDetail: { w: 800, h: 600, c: "fill", q: "auto", f: "auto" },
|
|
127
|
+
responsive: { w: "auto", dpr: "auto", q: "auto", f: "auto" }
|
|
128
|
+
};
|
|
129
|
+
export {
|
|
130
|
+
SanvikaCloudinaryProvider,
|
|
131
|
+
TRANSFORM_PRESETS,
|
|
132
|
+
extractPublicId,
|
|
133
|
+
getFolderPath,
|
|
134
|
+
getOptimizedUrl,
|
|
135
|
+
isCloudinaryUrl,
|
|
136
|
+
useCloudinaryUpload,
|
|
137
|
+
useSanvikaCloudinary,
|
|
138
|
+
validatePublicId
|
|
139
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
1
|
// src/cloudinaryCore.js
|
|
4
2
|
import { v2 as cloudinary } from "cloudinary";
|
|
5
3
|
|
|
@@ -148,10 +146,17 @@ async function uploadImage(fileOrBuffer, options = {}) {
|
|
|
148
146
|
folder,
|
|
149
147
|
resourceType = "image",
|
|
150
148
|
tags = [],
|
|
149
|
+
eager,
|
|
151
150
|
transforms,
|
|
151
|
+
eagerAsync,
|
|
152
|
+
eagerNotificationUrl,
|
|
152
153
|
publicId,
|
|
153
154
|
overwrite = false,
|
|
154
|
-
notificationUrl
|
|
155
|
+
notificationUrl,
|
|
156
|
+
chunked,
|
|
157
|
+
chunkSize = 6 * 1024 * 1024,
|
|
158
|
+
timeout,
|
|
159
|
+
maxAttempts = 3
|
|
155
160
|
} = options;
|
|
156
161
|
const uploadFolder = getFolderPath(_appName, folder);
|
|
157
162
|
const uploadOpts = {
|
|
@@ -161,22 +166,40 @@ async function uploadImage(fileOrBuffer, options = {}) {
|
|
|
161
166
|
tags: [_appName, ...tags]
|
|
162
167
|
};
|
|
163
168
|
if (publicId) uploadOpts.public_id = publicId;
|
|
164
|
-
|
|
169
|
+
const eagerValue = eager || transforms;
|
|
170
|
+
if (eagerValue) uploadOpts.eager = eagerValue;
|
|
171
|
+
if (eagerAsync) uploadOpts.eager_async = true;
|
|
172
|
+
if (eagerNotificationUrl) uploadOpts.eager_notification_url = eagerNotificationUrl;
|
|
165
173
|
if (notificationUrl) uploadOpts.notification_url = notificationUrl;
|
|
174
|
+
if (timeout) uploadOpts.timeout = timeout;
|
|
175
|
+
const fileSize = Buffer.isBuffer(fileOrBuffer) ? fileOrBuffer.length : null;
|
|
176
|
+
const useChunked = chunked || resourceType === "video" && fileSize !== null && fileSize > 10 * 1024 * 1024;
|
|
177
|
+
if (useChunked) {
|
|
178
|
+
uploadOpts.chunk_size = chunkSize;
|
|
179
|
+
}
|
|
166
180
|
return withRetry(
|
|
167
181
|
() => {
|
|
168
182
|
if (Buffer.isBuffer(fileOrBuffer)) {
|
|
169
183
|
return new Promise((resolve, reject) => {
|
|
170
|
-
const
|
|
184
|
+
const streamFactory = useChunked ? cloudinary.uploader.upload_chunked_stream : cloudinary.uploader.upload_stream;
|
|
185
|
+
const stream = streamFactory.call(cloudinary.uploader, uploadOpts, (err, result) => {
|
|
171
186
|
if (err) return reject(err);
|
|
172
187
|
resolve(result);
|
|
173
188
|
});
|
|
174
189
|
stream.end(fileOrBuffer);
|
|
175
190
|
});
|
|
176
191
|
}
|
|
192
|
+
if (useChunked) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
cloudinary.uploader.upload_large(fileOrBuffer, uploadOpts, (err, result) => {
|
|
195
|
+
if (err) return reject(err);
|
|
196
|
+
resolve(result);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
177
200
|
return cloudinary.uploader.upload(fileOrBuffer, uploadOpts);
|
|
178
201
|
},
|
|
179
|
-
{ operationName: "uploadImage", maxAttempts
|
|
202
|
+
{ operationName: "uploadImage", maxAttempts }
|
|
180
203
|
);
|
|
181
204
|
}
|
|
182
205
|
async function uploadVideo(fileOrBuffer, options = {}) {
|
|
@@ -255,99 +278,115 @@ async function pingCloudinary() {
|
|
|
255
278
|
ensureConfigured();
|
|
256
279
|
return cloudinary.api.ping();
|
|
257
280
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
var SanvikaCloudinaryContext = createContext(null);
|
|
263
|
-
function SanvikaCloudinaryProvider({ appName, cloudName, uploadPreset = "ml_default", children }) {
|
|
264
|
-
const value = useMemo(
|
|
265
|
-
() => ({ appName, cloudName, uploadPreset }),
|
|
266
|
-
[appName, cloudName, uploadPreset]
|
|
267
|
-
);
|
|
268
|
-
return /* @__PURE__ */ jsx(SanvikaCloudinaryContext.Provider, { value, children });
|
|
281
|
+
async function getCloudinaryUsage(options = {}) {
|
|
282
|
+
ensureConfigured();
|
|
283
|
+
const { resourceType } = options;
|
|
284
|
+
return cloudinary.api.usage(resourceType ? { resource_type: resourceType } : {});
|
|
269
285
|
}
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
if (!ctx) {
|
|
273
|
-
throw new Error("useSanvikaCloudinary must be used within a SanvikaCloudinaryProvider");
|
|
274
|
-
}
|
|
275
|
-
return ctx;
|
|
286
|
+
function getCloudinarySdkVersion() {
|
|
287
|
+
return cloudinary.CLOUDINARY_VERSION;
|
|
276
288
|
}
|
|
277
289
|
|
|
278
|
-
// src/
|
|
279
|
-
import
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const msg = (err == null ? void 0 : err.message) || "Upload failed";
|
|
321
|
-
setError(msg);
|
|
322
|
-
throw err;
|
|
323
|
-
} finally {
|
|
324
|
-
setUploading(false);
|
|
325
|
-
}
|
|
290
|
+
// src/cloudinaryDiagnostics.js
|
|
291
|
+
import crypto from "crypto";
|
|
292
|
+
function testCloudinaryWebhookSignature(apiSecret, options = {}) {
|
|
293
|
+
try {
|
|
294
|
+
if (!apiSecret) {
|
|
295
|
+
return { success: false, error: "apiSecret is required" };
|
|
296
|
+
}
|
|
297
|
+
const testPayload = { test: "data", timestamp: Math.floor(Date.now() / 1e3) };
|
|
298
|
+
const testPayloadString = JSON.stringify(testPayload);
|
|
299
|
+
const testTimestamp = testPayload.timestamp.toString();
|
|
300
|
+
const signatureData = testTimestamp + testPayloadString;
|
|
301
|
+
const signature = crypto.createHmac("sha1", apiSecret).update(signatureData).digest("hex");
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
testPayload,
|
|
305
|
+
testTimestamp,
|
|
306
|
+
signature,
|
|
307
|
+
headers: {
|
|
308
|
+
"X-Cld-Timestamp": testTimestamp,
|
|
309
|
+
"X-Cld-Signature": signature
|
|
310
|
+
},
|
|
311
|
+
testEndpoint: options.testEndpoint || "/api/webhooks/cloudinary/debug"
|
|
312
|
+
};
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return { success: false, error: error.message };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function runCloudinaryDiagnostics(options = {}) {
|
|
318
|
+
const {
|
|
319
|
+
testWebhook = false,
|
|
320
|
+
webhookSecret,
|
|
321
|
+
testFolder = "diagnostics"
|
|
322
|
+
} = options;
|
|
323
|
+
const report = {
|
|
324
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
325
|
+
cloudinaryVersion: getCloudinarySdkVersion(),
|
|
326
|
+
nodeVersion: process.version,
|
|
327
|
+
configuration: {
|
|
328
|
+
cloudName: process.env.CLOUDINARY_CLOUD_NAME || process.env.CLOUDINARY_NAME ? "set" : "missing",
|
|
329
|
+
apiKey: process.env.CLOUDINARY_API_KEY ? "set" : "missing",
|
|
330
|
+
apiSecret: process.env.CLOUDINARY_API_SECRET ? "set" : "missing",
|
|
331
|
+
uploadPreset: process.env.CLOUDINARY_UPLOAD_PRESET ? "set" : "missing"
|
|
326
332
|
},
|
|
327
|
-
|
|
333
|
+
tests: {}
|
|
334
|
+
};
|
|
335
|
+
report.tests.ping = await pingCloudinary().then(
|
|
336
|
+
(result) => ({ success: true, result }),
|
|
337
|
+
(error) => ({ success: false, error: error.message })
|
|
338
|
+
);
|
|
339
|
+
const testBuffer = Buffer.from("Cloudinary diagnostic test");
|
|
340
|
+
report.tests.upload = await uploadImage(testBuffer, {
|
|
341
|
+
folder: testFolder,
|
|
342
|
+
resourceType: "raw",
|
|
343
|
+
tags: ["test", "diagnostics"],
|
|
344
|
+
publicId: `diagnostic_${Date.now()}`,
|
|
345
|
+
overwrite: true
|
|
346
|
+
}).then(
|
|
347
|
+
(result) => ({
|
|
348
|
+
success: true,
|
|
349
|
+
publicId: result.public_id,
|
|
350
|
+
url: result.secure_url,
|
|
351
|
+
bytes: result.bytes
|
|
352
|
+
}),
|
|
353
|
+
(error) => ({
|
|
354
|
+
success: false,
|
|
355
|
+
error: error.message,
|
|
356
|
+
httpCode: error.http_code,
|
|
357
|
+
name: error.name
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
report.tests.videoLimits = await getCloudinaryUsage({ resourceType: "video" }).then(
|
|
361
|
+
(result) => ({ success: true, result }),
|
|
362
|
+
(error) => ({ success: false, error: error.message })
|
|
328
363
|
);
|
|
329
|
-
|
|
364
|
+
if (testWebhook) {
|
|
365
|
+
report.tests.webhook = testCloudinaryWebhookSignature(webhookSecret);
|
|
366
|
+
}
|
|
367
|
+
return report;
|
|
330
368
|
}
|
|
331
369
|
export {
|
|
332
370
|
CloudinaryError,
|
|
333
|
-
SanvikaCloudinaryProvider,
|
|
334
371
|
TRANSFORM_PRESETS,
|
|
335
372
|
configureSanvikaCloudinary,
|
|
336
373
|
deleteImage,
|
|
337
374
|
deleteImages,
|
|
338
375
|
extractPublicId,
|
|
339
376
|
getAppName,
|
|
377
|
+
getCloudinarySdkVersion,
|
|
378
|
+
getCloudinaryUsage,
|
|
340
379
|
getFolderPath,
|
|
341
380
|
getOptimizedUrl,
|
|
342
381
|
isCloudinaryUrl,
|
|
343
382
|
isRetriableError,
|
|
344
383
|
pingCloudinary,
|
|
384
|
+
runCloudinaryDiagnostics,
|
|
385
|
+
testCloudinaryWebhookSignature,
|
|
345
386
|
uploadImage,
|
|
346
387
|
uploadImages,
|
|
347
388
|
uploadRawFile,
|
|
348
389
|
uploadVideo,
|
|
349
|
-
useCloudinaryUpload,
|
|
350
|
-
useSanvikaCloudinary,
|
|
351
390
|
validatePublicId,
|
|
352
391
|
withRetry
|
|
353
392
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanvika/cloudinary",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Centralized Cloudinary SDK for the Sanvika ecosystem — upload, delete, transform, and manage media across 50+ projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./dist/index.js"
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./client": "./dist/client.js"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"dist"
|
|
@@ -36,8 +37,12 @@
|
|
|
36
37
|
"react-dom": ">=18.0.0"
|
|
37
38
|
},
|
|
38
39
|
"peerDependenciesMeta": {
|
|
39
|
-
"react": {
|
|
40
|
-
|
|
40
|
+
"react": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"react-dom": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
41
46
|
},
|
|
42
47
|
"engines": {
|
|
43
48
|
"node": ">=18.0.0"
|