@cobinar/dalus 0.1.3 → 0.1.5

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/src/storage.js ADDED
@@ -0,0 +1,72 @@
1
+ // src/storage.js
2
+ import { generateId } from './utils.js';
3
+
4
+ const FOLDER_COLLECTIONS = new Set(['posts', 'products', 'projects']);
5
+ const FLAT_COLLECTIONS = new Set(['images', 'files']);
6
+ export const ALL_COLLECTIONS = new Set([...FOLDER_COLLECTIONS, ...FLAT_COLLECTIONS]);
7
+ export { FOLDER_COLLECTIONS, FLAT_COLLECTIONS };
8
+
9
+ export function generateUploadPath(collection, entityId, ext, slot) {
10
+ const uid = generateId();
11
+ if (FLAT_COLLECTIONS.has(collection)) {
12
+ return `cobinar/${collection}/${uid}.${ext.toLowerCase()}`;
13
+ }
14
+ const prefix = slot ? `${slot}-${uid}` : uid;
15
+ return `cobinar/${collection}/${entityId}/${prefix}.${ext.toLowerCase()}`;
16
+ }
17
+
18
+ export async function uploadFile(bucket, key, body, contentType, metadata = {}) {
19
+ await bucket.put(key, body, {
20
+ httpMetadata: {
21
+ contentType,
22
+ cacheControl: 'public, max-age=31536000, immutable',
23
+ },
24
+ customMetadata: {
25
+ uploadedAt: new Date().toISOString(),
26
+ ...sanitizeMetadata(metadata),
27
+ },
28
+ });
29
+ return { key };
30
+ }
31
+
32
+ export async function deleteFiles(bucket, keys) {
33
+ if (!keys || keys.length === 0) return;
34
+ await bucket.delete(keys);
35
+ }
36
+
37
+ export async function streamFile(bucket, key, extraHeaders = {}) {
38
+ const object = await bucket.get(key);
39
+ if (!object) {
40
+ return new Response(JSON.stringify({ ok: false, error: 'File not found' }), {
41
+ status: 404,
42
+ headers: { 'Content-Type': 'application/json' },
43
+ });
44
+ }
45
+ const headers = new Headers({
46
+ 'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
47
+ 'Cache-Control': 'public, max-age=31536000, immutable',
48
+ ETag: object.etag,
49
+ 'Last-Modified': object.uploaded?.toUTCString() || '',
50
+ 'X-Served-By': 'cobinar-r2',
51
+ ...extraHeaders,
52
+ });
53
+ return new Response(object.body, { headers });
54
+ }
55
+
56
+ export function getPublicUrl(key, env) {
57
+ if (env.R2_PUBLIC_DOMAIN) {
58
+ return `https://${env.R2_PUBLIC_DOMAIN}/${key}`;
59
+ }
60
+ const domain = env.WORKER_DOMAIN || 'cobinar-r2.workers.dev';
61
+ return `https://${domain}/assets/${key}`;
62
+ }
63
+
64
+ function sanitizeMetadata(obj) {
65
+ const out = {};
66
+ for (const [k, v] of Object.entries(obj)) {
67
+ if (v !== null && v !== undefined) {
68
+ out[k] = String(v);
69
+ }
70
+ }
71
+ return out;
72
+ }
package/src/utils.js ADDED
@@ -0,0 +1,7 @@
1
+ // src/utils.js
2
+
3
+ export function generateId() {
4
+ const buf = new Uint8Array(8);
5
+ crypto.getRandomValues(buf);
6
+ return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('').slice(0, 12);
7
+ }
@@ -0,0 +1,101 @@
1
+ // src/validators.js
2
+
3
+ export const ALLOWED_TYPES = {
4
+ image: {
5
+ mimes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif', 'image/svg+xml'],
6
+ extensions: ['jpg', 'jpeg', 'png', 'webp', 'gif', 'avif', 'svg'],
7
+ maxBytes: 25 * 1024 * 1024,
8
+ label: 'Image',
9
+ },
10
+ projectImage: {
11
+ mimes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif'],
12
+ extensions: ['jpg', 'jpeg', 'png', 'webp', 'avif'],
13
+ maxBytes: 10 * 1024 * 1024,
14
+ label: 'Project image',
15
+ },
16
+ productImage: {
17
+ mimes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif'],
18
+ extensions: ['jpg', 'jpeg', 'png', 'webp', 'avif'],
19
+ maxBytes: 10 * 1024 * 1024,
20
+ label: 'Product image',
21
+ },
22
+ postMedia: {
23
+ mimes: [
24
+ 'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif',
25
+ 'video/mp4', 'video/webm', 'video/quicktime', 'application/pdf',
26
+ ],
27
+ extensions: ['jpg', 'jpeg', 'png', 'webp', 'gif', 'avif', 'mp4', 'webm', 'mov', 'pdf'],
28
+ maxBytes: 50 * 1024 * 1024,
29
+ label: 'Post media',
30
+ },
31
+ file: {
32
+ mimes: [
33
+ 'application/pdf', 'application/zip', 'application/x-zip-compressed',
34
+ 'application/octet-stream', 'text/plain', 'text/csv',
35
+ 'application/vnd.ms-excel',
36
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
37
+ 'application/msword',
38
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
39
+ 'application/json',
40
+ ],
41
+ extensions: ['pdf', 'zip', 'txt', 'csv', 'xls', 'xlsx', 'doc', 'docx', 'json', 'bin'],
42
+ maxBytes: 100 * 1024 * 1024,
43
+ label: 'File',
44
+ },
45
+ };
46
+
47
+ export const DEFAULT_RULE = {
48
+ posts: 'postMedia',
49
+ products: 'productImage',
50
+ projects: 'projectImage',
51
+ images: 'image',
52
+ files: 'file',
53
+ };
54
+
55
+ export function validateFile(mimeType, sizeBytes, filename, ruleName) {
56
+ const rule = ALLOWED_TYPES[ruleName];
57
+ if (!rule) return { valid: false, error: `Unknown validation rule: "${ruleName}"` };
58
+
59
+ const baseMime = mimeType.split(';')[0].trim().toLowerCase();
60
+ if (!rule.mimes.includes(baseMime)) {
61
+ return { valid: false, error: `${rule.label} must be one of: ${rule.mimes.join(', ')}. Got: ${baseMime}` };
62
+ }
63
+
64
+ const rawExt = (filename.split('.').pop() || '').toLowerCase();
65
+ if (!rule.extensions.includes(rawExt)) {
66
+ return { valid: false, error: `${rule.label} extension must be .${rule.extensions.join(', .')}. Got: .${rawExt}` };
67
+ }
68
+
69
+ if (sizeBytes > rule.maxBytes) {
70
+ const maxMb = (rule.maxBytes / 1024 / 1024).toFixed(0);
71
+ const gotMb = (sizeBytes / 1024 / 1024).toFixed(1);
72
+ return { valid: false, error: `${rule.label} must be under ${maxMb} MB. Got: ${gotMb} MB` };
73
+ }
74
+
75
+ return { valid: true, ext: rawExt };
76
+ }
77
+
78
+ export function extFromMime(mimeType) {
79
+ const map = {
80
+ 'image/jpeg': 'jpg',
81
+ 'image/png': 'png',
82
+ 'image/webp': 'webp',
83
+ 'image/gif': 'gif',
84
+ 'image/avif': 'avif',
85
+ 'image/svg+xml': 'svg',
86
+ 'video/mp4': 'mp4',
87
+ 'video/webm': 'webm',
88
+ 'video/quicktime': 'mov',
89
+ 'application/pdf': 'pdf',
90
+ 'application/zip': 'zip',
91
+ 'application/x-zip-compressed': 'zip',
92
+ 'text/plain': 'txt',
93
+ 'text/csv': 'csv',
94
+ 'application/vnd.ms-excel': 'xls',
95
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
96
+ 'application/msword': 'doc',
97
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
98
+ 'application/json': 'json',
99
+ };
100
+ return map[mimeType.split(';')[0].trim().toLowerCase()] || 'bin';
101
+ }