openkbs 0.0.64 → 0.0.66
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/elastic/README.md +1150 -0
- package/elastic/functions.md +328 -0
- package/elastic/postgres.md +287 -0
- package/elastic/pulse.md +386 -0
- package/elastic/storage.md +291 -0
- package/package.json +1 -1
- package/src/actions.js +76 -92
- package/src/index.js +23 -10
- package/src/utils.js +8 -4
- package/templates/.claude/skills/openkbs/SKILL.md +184 -0
- package/templates/.claude/skills/openkbs/metadata.json +1 -0
- package/templates/.claude/skills/openkbs/reference/backend-sdk.md +428 -0
- package/templates/.claude/skills/openkbs/reference/commands.md +370 -0
- package/templates/.claude/skills/openkbs/reference/elastic-services.md +327 -0
- package/templates/.claude/skills/openkbs/reference/frontend-sdk.md +299 -0
- package/version.json +3 -3
- package/templates/.openkbs/knowledge/metadata.json +0 -3
- package/templates/CLAUDE.md +0 -655
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/app/icon.png +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/app/instructions.txt +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/app/settings.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/scripts/run_job.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/scripts/utils/agent_client.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/actions.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/handler.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onRequest.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onRequest.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onResponse.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onResponse.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Frontend/contentRender.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Frontend/contentRender.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/README.md +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/app/instructions.txt +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/app/settings.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/actions.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onRequest.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onRequest.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onResponse.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onResponse.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Frontend/contentRender.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Frontend/contentRender.json +0 -0
package/elastic/pulse.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Tutorial 14: Real-time with Pulse
|
|
2
|
+
|
|
3
|
+
Add real-time features to your app with Pulse WebSocket messaging. Build live updates, chat, presence tracking, and collaborative features.
|
|
4
|
+
|
|
5
|
+
## Enable Pulse
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openkbs pulse enable
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
15
|
+
│ Browser 1 │ │ Pulse │ │ Browser 2 │
|
|
16
|
+
│ │────▶│ Server │────▶│ │
|
|
17
|
+
│ Subscribe │ │ │ │ Receive │
|
|
18
|
+
│ to 'posts' │ │ │ │ message │
|
|
19
|
+
└──────────────┘ └──────────────┘ └──────────────┘
|
|
20
|
+
▲
|
|
21
|
+
│ Publish
|
|
22
|
+
┌──────────────┐
|
|
23
|
+
│ Lambda │
|
|
24
|
+
│ Function │
|
|
25
|
+
└──────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
1. Clients connect via WebSocket and subscribe to channels
|
|
29
|
+
2. Your Lambda publishes events to channels
|
|
30
|
+
3. All subscribers receive the message instantly
|
|
31
|
+
|
|
32
|
+
## Client SDK (Browser)
|
|
33
|
+
|
|
34
|
+
### Install
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<script src="https://unpkg.com/openkbs-pulse@2.0.1/pulse.js"></script>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or with npm:
|
|
41
|
+
```bash
|
|
42
|
+
npm install openkbs-pulse
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Connect
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
// Get token from your API (see Server SDK below)
|
|
49
|
+
const { token, endpoint, kbId } = await fetch('/auth', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
body: JSON.stringify({ action: 'getPulseToken', userId: 'user123' })
|
|
52
|
+
}).then(r => r.json());
|
|
53
|
+
|
|
54
|
+
// Connect to Pulse
|
|
55
|
+
const realtime = new Pulse.Realtime({
|
|
56
|
+
kbId,
|
|
57
|
+
token,
|
|
58
|
+
endpoint,
|
|
59
|
+
clientId: 'user123'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Connection events
|
|
63
|
+
realtime.connection.on('connected', () => console.log('Connected!'));
|
|
64
|
+
realtime.connection.on('disconnected', () => console.log('Disconnected'));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Subscribe to Channel
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
const channel = realtime.channels.get('posts');
|
|
71
|
+
|
|
72
|
+
// Subscribe to specific event
|
|
73
|
+
channel.subscribe('new_post', (message) => {
|
|
74
|
+
console.log('New post:', message.data);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Subscribe to all events
|
|
78
|
+
channel.subscribe((message) => {
|
|
79
|
+
console.log('Event:', message.name, message.data);
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Presence (Who's Online)
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
const channel = realtime.channels.get('posts');
|
|
87
|
+
|
|
88
|
+
// Enter presence with data
|
|
89
|
+
channel.presence.enter({ name: 'Alice', avatar: '/alice.jpg' });
|
|
90
|
+
|
|
91
|
+
// Get current members
|
|
92
|
+
channel.presence.get((members) => {
|
|
93
|
+
console.log('Online:', members.length);
|
|
94
|
+
members.forEach(m => console.log(m.data.name));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Subscribe to presence changes
|
|
98
|
+
channel.presence.subscribe((members) => {
|
|
99
|
+
console.log('Members updated:', members);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Specific events
|
|
103
|
+
channel.presence.subscribe('enter', (member) => {
|
|
104
|
+
console.log(`${member.data.name} joined`);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
channel.presence.subscribe('leave', (member) => {
|
|
108
|
+
console.log(`${member.data.name} left`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Leave when done
|
|
112
|
+
channel.presence.leave();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Disconnect
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
realtime.close();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Server SDK (Lambda)
|
|
122
|
+
|
|
123
|
+
### Generate Token
|
|
124
|
+
|
|
125
|
+
Your Lambda must generate tokens for clients:
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
import pulse from 'openkbs-pulse/server';
|
|
129
|
+
|
|
130
|
+
export async function handler(event) {
|
|
131
|
+
const { action, userId } = JSON.parse(event.body || '{}');
|
|
132
|
+
const kbId = process.env.OPENKBS_KB_ID;
|
|
133
|
+
const apiKey = process.env.OPENKBS_API_KEY;
|
|
134
|
+
|
|
135
|
+
if (action === 'getPulseToken') {
|
|
136
|
+
const tokenData = await pulse.getToken(kbId, apiKey, userId);
|
|
137
|
+
return {
|
|
138
|
+
statusCode: 200,
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
token: tokenData.token,
|
|
141
|
+
endpoint: tokenData.endpoint,
|
|
142
|
+
kbId
|
|
143
|
+
})
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Publish Events
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
import pulse from 'openkbs-pulse/server';
|
|
153
|
+
|
|
154
|
+
export async function handler(event) {
|
|
155
|
+
const { action, ...data } = JSON.parse(event.body || '{}');
|
|
156
|
+
const kbId = process.env.OPENKBS_KB_ID;
|
|
157
|
+
const apiKey = process.env.OPENKBS_API_KEY;
|
|
158
|
+
|
|
159
|
+
if (action === 'createPost') {
|
|
160
|
+
// Save to database...
|
|
161
|
+
const post = { id: 1, title: data.title, content: data.content };
|
|
162
|
+
|
|
163
|
+
// Publish to all subscribers
|
|
164
|
+
await pulse.publish('posts', 'new_post', { post }, { kbId, apiKey });
|
|
165
|
+
|
|
166
|
+
return { statusCode: 200, body: JSON.stringify({ post }) };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Get Presence
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
const presence = await pulse.presence('posts', { kbId, apiKey });
|
|
175
|
+
console.log('Online count:', presence.count);
|
|
176
|
+
console.log('Members:', presence.members);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Complete Example
|
|
180
|
+
|
|
181
|
+
### Backend (posts/index.mjs)
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
import pg from 'pg';
|
|
185
|
+
import pulse from 'openkbs-pulse/server';
|
|
186
|
+
|
|
187
|
+
const db = new pg.Client({ connectionString: process.env.DATABASE_URL });
|
|
188
|
+
let connected = false;
|
|
189
|
+
|
|
190
|
+
export async function handler(event) {
|
|
191
|
+
if (!connected) {
|
|
192
|
+
await db.connect();
|
|
193
|
+
connected = true;
|
|
194
|
+
await db.query(`
|
|
195
|
+
CREATE TABLE IF NOT EXISTS posts (
|
|
196
|
+
id SERIAL PRIMARY KEY,
|
|
197
|
+
user_name TEXT,
|
|
198
|
+
content TEXT,
|
|
199
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
200
|
+
)
|
|
201
|
+
`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const headers = {
|
|
205
|
+
'Content-Type': 'application/json',
|
|
206
|
+
'Access-Control-Allow-Origin': '*',
|
|
207
|
+
'Access-Control-Allow-Headers': 'Content-Type'
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (event.requestContext?.http?.method === 'OPTIONS') {
|
|
211
|
+
return { statusCode: 200, headers, body: '' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const { action, ...data } = JSON.parse(event.body || '{}');
|
|
215
|
+
const kbId = process.env.OPENKBS_KB_ID;
|
|
216
|
+
const apiKey = process.env.OPENKBS_API_KEY;
|
|
217
|
+
|
|
218
|
+
switch (action) {
|
|
219
|
+
case 'getPulseToken': {
|
|
220
|
+
const tokenData = await pulse.getToken(kbId, apiKey, data.userId);
|
|
221
|
+
return {
|
|
222
|
+
statusCode: 200,
|
|
223
|
+
headers,
|
|
224
|
+
body: JSON.stringify({ ...tokenData, kbId })
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case 'list': {
|
|
229
|
+
const { rows } = await db.query(
|
|
230
|
+
'SELECT * FROM posts ORDER BY created_at DESC LIMIT 50'
|
|
231
|
+
);
|
|
232
|
+
return { statusCode: 200, headers, body: JSON.stringify({ posts: rows }) };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
case 'create': {
|
|
236
|
+
const result = await db.query(
|
|
237
|
+
'INSERT INTO posts (user_name, content) VALUES ($1, $2) RETURNING *',
|
|
238
|
+
[data.userName, data.content]
|
|
239
|
+
);
|
|
240
|
+
const post = result.rows[0];
|
|
241
|
+
|
|
242
|
+
// Broadcast to all subscribers
|
|
243
|
+
await pulse.publish('posts', 'new_post', { post }, { kbId, apiKey });
|
|
244
|
+
|
|
245
|
+
return { statusCode: 200, headers, body: JSON.stringify({ post }) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case 'presence': {
|
|
249
|
+
const result = await pulse.presence('posts', { kbId, apiKey });
|
|
250
|
+
return { statusCode: 200, headers, body: JSON.stringify(result) };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
default:
|
|
254
|
+
return { statusCode: 400, headers, body: JSON.stringify({ error: 'Unknown action' }) };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Frontend
|
|
260
|
+
|
|
261
|
+
```html
|
|
262
|
+
<!DOCTYPE html>
|
|
263
|
+
<html>
|
|
264
|
+
<head>
|
|
265
|
+
<title>Live Posts</title>
|
|
266
|
+
<script src="https://unpkg.com/openkbs-pulse@2.0.1/pulse.js"></script>
|
|
267
|
+
</head>
|
|
268
|
+
<body>
|
|
269
|
+
<div id="online">Online: 0</div>
|
|
270
|
+
<div id="posts"></div>
|
|
271
|
+
|
|
272
|
+
<form id="form">
|
|
273
|
+
<input type="text" id="content" placeholder="What's on your mind?">
|
|
274
|
+
<button type="submit">Post</button>
|
|
275
|
+
</form>
|
|
276
|
+
|
|
277
|
+
<script>
|
|
278
|
+
const userId = 'user_' + Math.random().toString(36).substr(2, 9);
|
|
279
|
+
let realtime;
|
|
280
|
+
|
|
281
|
+
async function init() {
|
|
282
|
+
// Load existing posts
|
|
283
|
+
const { posts } = await fetch('/posts', {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: { 'Content-Type': 'application/json' },
|
|
286
|
+
body: JSON.stringify({ action: 'list' })
|
|
287
|
+
}).then(r => r.json());
|
|
288
|
+
|
|
289
|
+
posts.forEach(addPost);
|
|
290
|
+
|
|
291
|
+
// Get Pulse token
|
|
292
|
+
const { token, endpoint, kbId } = await fetch('/posts', {
|
|
293
|
+
method: 'POST',
|
|
294
|
+
headers: { 'Content-Type': 'application/json' },
|
|
295
|
+
body: JSON.stringify({ action: 'getPulseToken', userId })
|
|
296
|
+
}).then(r => r.json());
|
|
297
|
+
|
|
298
|
+
// Connect
|
|
299
|
+
realtime = new Pulse.Realtime({ kbId, token, endpoint, clientId: userId });
|
|
300
|
+
|
|
301
|
+
const channel = realtime.channels.get('posts');
|
|
302
|
+
|
|
303
|
+
// Subscribe to new posts
|
|
304
|
+
channel.subscribe('new_post', (msg) => {
|
|
305
|
+
addPost(msg.data.post);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Presence
|
|
309
|
+
channel.presence.enter({ name: userId });
|
|
310
|
+
channel.presence.subscribe((members) => {
|
|
311
|
+
document.getElementById('online').textContent = 'Online: ' + members.length;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function addPost(post) {
|
|
316
|
+
const div = document.createElement('div');
|
|
317
|
+
div.innerHTML = `<b>${post.user_name}</b>: ${post.content}`;
|
|
318
|
+
document.getElementById('posts').prepend(div);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
document.getElementById('form').addEventListener('submit', async (e) => {
|
|
322
|
+
e.preventDefault();
|
|
323
|
+
const content = document.getElementById('content').value;
|
|
324
|
+
if (!content) return;
|
|
325
|
+
|
|
326
|
+
await fetch('/posts', {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
headers: { 'Content-Type': 'application/json' },
|
|
329
|
+
body: JSON.stringify({ action: 'create', content, userName: userId })
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
document.getElementById('content').value = '';
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
init();
|
|
336
|
+
</script>
|
|
337
|
+
</body>
|
|
338
|
+
</html>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Private Channels
|
|
342
|
+
|
|
343
|
+
For private messaging, use secret channel IDs:
|
|
344
|
+
|
|
345
|
+
```javascript
|
|
346
|
+
// Generate unique channel ID per user (store in database)
|
|
347
|
+
const privateChannel = crypto.randomBytes(32).toString('hex');
|
|
348
|
+
|
|
349
|
+
// User subscribes to their own private channel
|
|
350
|
+
const myChannel = realtime.channels.get(user.privateChannel);
|
|
351
|
+
myChannel.subscribe('message', (msg) => {
|
|
352
|
+
console.log('Private message:', msg.data);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Server publishes to recipient's channel
|
|
356
|
+
await pulse.publish(recipient.privateChannel, 'message', {
|
|
357
|
+
from: sender.name,
|
|
358
|
+
text: 'Hello!'
|
|
359
|
+
}, { kbId, apiKey });
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## CLI Reference
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
openkbs pulse enable # Enable Pulse
|
|
366
|
+
openkbs pulse status # Check status
|
|
367
|
+
openkbs pulse channels # List active channels
|
|
368
|
+
openkbs pulse presence <channel> # View presence
|
|
369
|
+
openkbs pulse publish <ch> "msg" # Publish message
|
|
370
|
+
openkbs pulse disable # Disable
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Tips
|
|
374
|
+
|
|
375
|
+
1. **Generate Tokens Server-Side** - Never expose your API key in frontend code.
|
|
376
|
+
|
|
377
|
+
2. **Use Presence for Online Status** - Built-in, no database needed.
|
|
378
|
+
|
|
379
|
+
3. **Private Channels** - Use random IDs for 1:1 messaging.
|
|
380
|
+
|
|
381
|
+
4. **Reconnection** - SDK auto-reconnects with exponential backoff.
|
|
382
|
+
|
|
383
|
+
## Next Steps
|
|
384
|
+
|
|
385
|
+
- [Tutorial 15: Node.js Full Example](./15-nodejs-example.md)
|
|
386
|
+
- [Tutorial 16: Java Example](./16-java-example.md)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Tutorial 12: S3 Storage with CloudFront CDN
|
|
2
|
+
|
|
3
|
+
Upload files, serve images, and host media with S3 storage and CloudFront CDN. Get presigned URLs for secure browser uploads.
|
|
4
|
+
|
|
5
|
+
## Enable Storage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openkbs storage enable
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Your S3 bucket is ready.
|
|
12
|
+
|
|
13
|
+
## Check Status
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
openkbs storage status
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Output:
|
|
20
|
+
```
|
|
21
|
+
Storage Status:
|
|
22
|
+
Enabled: true
|
|
23
|
+
Bucket: openkbs-elastic-abc123
|
|
24
|
+
Region: us-east-1
|
|
25
|
+
Public: false
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Add CloudFront CDN
|
|
29
|
+
|
|
30
|
+
Serve files from your domain with edge caching:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
openkbs storage cloudfront media
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This maps:
|
|
37
|
+
- S3 path `media/*` to URL `yourdomain.com/media/*`
|
|
38
|
+
|
|
39
|
+
| S3 Key | Public URL |
|
|
40
|
+
|--------|------------|
|
|
41
|
+
| `media/photo.jpg` | `yourdomain.com/media/photo.jpg` |
|
|
42
|
+
| `media/uploads/image.png` | `yourdomain.com/media/uploads/image.png` |
|
|
43
|
+
|
|
44
|
+
## Upload Files
|
|
45
|
+
|
|
46
|
+
### From CLI
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
openkbs storage put ./photo.jpg media/photo.jpg
|
|
50
|
+
openkbs storage put ./document.pdf docs/document.pdf
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### From Lambda (Presigned URL)
|
|
54
|
+
|
|
55
|
+
Generate a presigned URL for browser uploads:
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
59
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
60
|
+
|
|
61
|
+
const s3 = new S3Client({ region: process.env.STORAGE_REGION });
|
|
62
|
+
|
|
63
|
+
export async function handler(event) {
|
|
64
|
+
const { action, fileName, contentType } = JSON.parse(event.body || '{}');
|
|
65
|
+
|
|
66
|
+
if (action === 'getUploadUrl') {
|
|
67
|
+
const bucket = process.env.STORAGE_BUCKET;
|
|
68
|
+
|
|
69
|
+
// Key must match CloudFront path prefix
|
|
70
|
+
const key = `media/uploads/${Date.now()}-${fileName}`;
|
|
71
|
+
|
|
72
|
+
const command = new PutObjectCommand({
|
|
73
|
+
Bucket: bucket,
|
|
74
|
+
Key: key,
|
|
75
|
+
ContentType: contentType || 'application/octet-stream'
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
|
|
79
|
+
|
|
80
|
+
// Return relative URL for CloudFront
|
|
81
|
+
const publicUrl = `/${key}`;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
statusCode: 200,
|
|
85
|
+
body: JSON.stringify({ uploadUrl, publicUrl, key })
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`package.json`:
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"type": "module",
|
|
95
|
+
"dependencies": {
|
|
96
|
+
"@aws-sdk/client-s3": "^3.400.0",
|
|
97
|
+
"@aws-sdk/s3-request-presigner": "^3.400.0"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Browser Upload
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
async function uploadFile(file) {
|
|
106
|
+
// 1. Get presigned URL from your API
|
|
107
|
+
const response = await fetch('/api', {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
action: 'getUploadUrl',
|
|
112
|
+
fileName: file.name,
|
|
113
|
+
contentType: file.type
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const { uploadUrl, publicUrl } = await response.json();
|
|
118
|
+
|
|
119
|
+
// 2. Upload directly to S3
|
|
120
|
+
await fetch(uploadUrl, {
|
|
121
|
+
method: 'PUT',
|
|
122
|
+
body: file,
|
|
123
|
+
headers: { 'Content-Type': file.type }
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// 3. Return the public URL
|
|
127
|
+
return publicUrl; // e.g., /media/uploads/1234567890-photo.jpg
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Usage
|
|
131
|
+
const fileInput = document.querySelector('input[type="file"]');
|
|
132
|
+
fileInput.addEventListener('change', async (e) => {
|
|
133
|
+
const file = e.target.files[0];
|
|
134
|
+
const url = await uploadFile(file);
|
|
135
|
+
console.log('Uploaded to:', url);
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Upload with Progress
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
function uploadWithProgress(file, onProgress) {
|
|
143
|
+
return new Promise(async (resolve, reject) => {
|
|
144
|
+
const { uploadUrl, publicUrl } = await fetch('/api', {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
action: 'getUploadUrl',
|
|
149
|
+
fileName: file.name,
|
|
150
|
+
contentType: file.type
|
|
151
|
+
})
|
|
152
|
+
}).then(r => r.json());
|
|
153
|
+
|
|
154
|
+
const xhr = new XMLHttpRequest();
|
|
155
|
+
|
|
156
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
157
|
+
if (e.lengthComputable) {
|
|
158
|
+
onProgress(Math.round((e.loaded / e.total) * 100));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
xhr.addEventListener('load', () => {
|
|
163
|
+
if (xhr.status === 200) resolve(publicUrl);
|
|
164
|
+
else reject(new Error('Upload failed'));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
xhr.addEventListener('error', reject);
|
|
168
|
+
xhr.open('PUT', uploadUrl);
|
|
169
|
+
xhr.setRequestHeader('Content-Type', file.type);
|
|
170
|
+
xhr.send(file);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Usage
|
|
175
|
+
const url = await uploadWithProgress(file, (percent) => {
|
|
176
|
+
console.log(`Upload: ${percent}%`);
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## List Files
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
openkbs storage ls
|
|
184
|
+
openkbs storage ls media/
|
|
185
|
+
openkbs storage ls media/uploads/ --limit 100
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Download Files
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
openkbs storage get media/photo.jpg
|
|
192
|
+
openkbs storage get media/photo.jpg ./local-photo.jpg
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Delete Files
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
openkbs storage rm media/photo.jpg
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Make Bucket Public
|
|
202
|
+
|
|
203
|
+
By default, buckets are private. To make files publicly accessible without presigned URLs:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
openkbs storage public true
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
To make private again:
|
|
210
|
+
```bash
|
|
211
|
+
openkbs storage public false
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## File Organization
|
|
215
|
+
|
|
216
|
+
Recommended structure:
|
|
217
|
+
```
|
|
218
|
+
media/
|
|
219
|
+
images/
|
|
220
|
+
avatars/
|
|
221
|
+
posts/
|
|
222
|
+
videos/
|
|
223
|
+
documents/
|
|
224
|
+
uploads/
|
|
225
|
+
temp/
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Python Example
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
import json
|
|
232
|
+
import os
|
|
233
|
+
import boto3
|
|
234
|
+
from botocore.config import Config
|
|
235
|
+
|
|
236
|
+
s3 = boto3.client('s3',
|
|
237
|
+
region_name=os.environ.get('STORAGE_REGION', 'us-east-1'),
|
|
238
|
+
config=Config(signature_version='s3v4')
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def handler(event, context):
|
|
242
|
+
body = json.loads(event.get('body', '{}'))
|
|
243
|
+
action = body.get('action')
|
|
244
|
+
bucket = os.environ['STORAGE_BUCKET']
|
|
245
|
+
|
|
246
|
+
if action == 'getUploadUrl':
|
|
247
|
+
import time
|
|
248
|
+
key = f"media/uploads/{int(time.time())}-{body['fileName']}"
|
|
249
|
+
|
|
250
|
+
url = s3.generate_presigned_url('put_object',
|
|
251
|
+
Params={'Bucket': bucket, 'Key': key, 'ContentType': body.get('contentType', 'application/octet-stream')},
|
|
252
|
+
ExpiresIn=3600
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
'statusCode': 200,
|
|
257
|
+
'body': json.dumps({'uploadUrl': url, 'publicUrl': f'/{key}', 'key': key})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {'statusCode': 400, 'body': json.dumps({'error': 'Unknown action'})}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## CLI Reference
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
openkbs storage enable # Enable storage
|
|
267
|
+
openkbs storage status # Check status
|
|
268
|
+
openkbs storage public true|false # Set public access
|
|
269
|
+
openkbs storage cloudfront <path> # Add CloudFront path
|
|
270
|
+
openkbs storage cloudfront remove <path> # Remove CloudFront path
|
|
271
|
+
openkbs storage ls [prefix] # List files
|
|
272
|
+
openkbs storage put <file> [key] # Upload file
|
|
273
|
+
openkbs storage get <key> [file] # Download file
|
|
274
|
+
openkbs storage rm <key> # Delete file
|
|
275
|
+
openkbs storage disable --force # Disable (DANGEROUS)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Tips
|
|
279
|
+
|
|
280
|
+
1. **Always Use Presigned URLs** - Don't send file data through your Lambda. Upload directly to S3.
|
|
281
|
+
|
|
282
|
+
2. **Match CloudFront Path** - If CloudFront path is `media`, upload to `media/...` keys.
|
|
283
|
+
|
|
284
|
+
3. **Use Content-Type** - Set correct MIME type for proper browser handling.
|
|
285
|
+
|
|
286
|
+
4. **Versioned Filenames** - Use timestamps or UUIDs to avoid cache issues: `1704067200-photo.jpg`
|
|
287
|
+
|
|
288
|
+
## Next Steps
|
|
289
|
+
|
|
290
|
+
- [Tutorial 13: Serverless Functions](./13-functions.md)
|
|
291
|
+
- [Tutorial 14: Real-time Pulse](./14-pulse.md)
|