@sproutboat/runtime 0.7.1 → 0.9.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 +36 -0
- package/README.md +41 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +58 -0
- package/src/r2-object.test.js +28 -2
- package/src/transport-broker.js +21 -0
- package/src/transport-embedded.js +813 -54
- package/src/wrap.ts +27 -6
|
@@ -41,6 +41,8 @@ extern int sqlite3_column_type(sqlite3_stmt*, int);
|
|
|
41
41
|
extern const unsigned char* sqlite3_column_text(sqlite3_stmt*, int);
|
|
42
42
|
extern int sqlite3_column_bytes(sqlite3_stmt*, int);
|
|
43
43
|
extern double sqlite3_column_double(sqlite3_stmt*, int);
|
|
44
|
+
extern int sqlite3_column_int(sqlite3_stmt*, int);
|
|
45
|
+
extern int64_t sqlite3_column_int64(sqlite3_stmt*, int);
|
|
44
46
|
extern const char* sqlite3_column_name(sqlite3_stmt*, int);
|
|
45
47
|
extern int sqlite3_bind_null(sqlite3_stmt*, int);
|
|
46
48
|
extern int sqlite3_bind_double(sqlite3_stmt*, int, double);
|
|
@@ -281,7 +283,8 @@ static void sb_iso_now(char* out, size_t cap) {
|
|
|
281
283
|
|
|
282
284
|
// "<dir-of-store_path>/r2-blobs/<sha256(bucket 0x1e key)>.blob" — deterministic,
|
|
283
285
|
// so no extra column is needed to find an object's file back.
|
|
284
|
-
static void sb_r2_blob_path(char* out, size_t cap, const char* store_path, const char* bucket, const char* key
|
|
286
|
+
static void sb_r2_blob_path(char* out, size_t cap, const char* store_path, const char* bucket, const char* key,
|
|
287
|
+
const char* generation) {
|
|
285
288
|
const char* slash = strrchr(store_path, '/');
|
|
286
289
|
int dirlen = slash ? (int)(slash - store_path) : 0;
|
|
287
290
|
br_sha256_context ctx;
|
|
@@ -289,6 +292,10 @@ static void sb_r2_blob_path(char* out, size_t cap, const char* store_path, const
|
|
|
289
292
|
br_sha256_update(&ctx, bucket, strlen(bucket));
|
|
290
293
|
br_sha256_update(&ctx, "\x1e", 1); // separator: a bucket/key split never collides across it
|
|
291
294
|
br_sha256_update(&ctx, key, strlen(key));
|
|
295
|
+
if (generation && *generation) {
|
|
296
|
+
br_sha256_update(&ctx, "\x1e", 1);
|
|
297
|
+
br_sha256_update(&ctx, generation, strlen(generation));
|
|
298
|
+
}
|
|
292
299
|
unsigned char digest[32];
|
|
293
300
|
br_sha256_out(&ctx, digest);
|
|
294
301
|
char hex[65];
|
|
@@ -296,6 +303,149 @@ static void sb_r2_blob_path(char* out, size_t cap, const char* store_path, const
|
|
|
296
303
|
snprintf(out, cap, "%.*s/r2-blobs/%s.blob", dirlen, store_path, hex);
|
|
297
304
|
}
|
|
298
305
|
|
|
306
|
+
static void sb_r2_part_path(char* out, size_t cap, const char* store_path, const char* bucket, const char* key,
|
|
307
|
+
const char* upload_id, int part_number) {
|
|
308
|
+
const char* slash = strrchr(store_path, '/');
|
|
309
|
+
int dirlen = slash ? (int)(slash - store_path) : 0;
|
|
310
|
+
br_sha256_context ctx;
|
|
311
|
+
br_sha256_init(&ctx);
|
|
312
|
+
br_sha256_update(&ctx, bucket, strlen(bucket));
|
|
313
|
+
br_sha256_update(&ctx, "\x1e", 1);
|
|
314
|
+
br_sha256_update(&ctx, key, strlen(key));
|
|
315
|
+
br_sha256_update(&ctx, "\x1e", 1);
|
|
316
|
+
br_sha256_update(&ctx, upload_id, strlen(upload_id));
|
|
317
|
+
br_sha256_update(&ctx, "\x1e", 1);
|
|
318
|
+
char number[16];
|
|
319
|
+
int number_len = snprintf(number, sizeof(number), "%d", part_number);
|
|
320
|
+
br_sha256_update(&ctx, number, (size_t)number_len);
|
|
321
|
+
unsigned char digest[32];
|
|
322
|
+
br_sha256_out(&ctx, digest);
|
|
323
|
+
char hex[65];
|
|
324
|
+
sb_hex32(digest, hex);
|
|
325
|
+
snprintf(out, cap, "%.*s/r2-parts/%s.part", dirlen, store_path, hex);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
static char* sb_r2_part_put_c(const char* path, const char* bucket, const char* key, const char* upload_id,
|
|
329
|
+
int part_number, const char* body, size_t bodylen) {
|
|
330
|
+
sb_buf out = { 0, 0, 0 };
|
|
331
|
+
char partpath[1024];
|
|
332
|
+
sb_r2_part_path(partpath, sizeof(partpath), path, bucket, key, upload_id, part_number);
|
|
333
|
+
sb_mkdirs(partpath);
|
|
334
|
+
char temporary[1100];
|
|
335
|
+
snprintf(temporary, sizeof(temporary), "%s.tmp", partpath);
|
|
336
|
+
FILE* f = fopen(temporary, "wb");
|
|
337
|
+
if (!f) { sb_puts(&out, "{\"ok\":false,\"error\":\"cannot write multipart part\"}"); return out.p; }
|
|
338
|
+
size_t written = bodylen ? fwrite(body, 1, bodylen, f) : 0;
|
|
339
|
+
int close_rc = fclose(f);
|
|
340
|
+
if (written != bodylen || close_rc != 0 || rename(temporary, partpath) != 0) {
|
|
341
|
+
unlink(temporary);
|
|
342
|
+
sb_puts(&out, "{\"ok\":false,\"error\":\"multipart part write failed\"}");
|
|
343
|
+
return out.p;
|
|
344
|
+
}
|
|
345
|
+
char etag[65];
|
|
346
|
+
sb_sha256_hex(body, bodylen, etag);
|
|
347
|
+
char result[160];
|
|
348
|
+
int n = snprintf(result, sizeof(result), "{\"ok\":true,\"etag\":\"%s\",\"size\":%zu}", etag, bodylen);
|
|
349
|
+
sb_put(&out, result, (size_t)n);
|
|
350
|
+
return out.p;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
static char* sb_r2_multipart_complete_c(const char* path, const char* bucket, const char* key, const char* upload_id) {
|
|
354
|
+
sb_buf out = { 0, 0, 0 };
|
|
355
|
+
int idx = sb_db_for(path);
|
|
356
|
+
if (idx < 0) { sb_puts(&out, "{\"ok\":false,\"error\":\"cannot open database\"}"); return out.p; }
|
|
357
|
+
|
|
358
|
+
char blobpath[1024];
|
|
359
|
+
sb_r2_blob_path(blobpath, sizeof(blobpath), path, bucket, key, "");
|
|
360
|
+
sb_mkdirs(blobpath);
|
|
361
|
+
char temporary[1100];
|
|
362
|
+
snprintf(temporary, sizeof(temporary), "%s.%s.tmp", blobpath, upload_id);
|
|
363
|
+
FILE* destination = fopen(temporary, "wb");
|
|
364
|
+
if (!destination) { sb_puts(&out, "{\"ok\":false,\"error\":\"cannot write object\"}"); return out.p; }
|
|
365
|
+
|
|
366
|
+
sqlite3_stmt* st = 0;
|
|
367
|
+
const char* sql =
|
|
368
|
+
"SELECT part_number, size FROM r2_part WHERE bucket = ?1 AND key = ?2 AND upload_id = ?3 ORDER BY part_number";
|
|
369
|
+
if (sqlite3_prepare_v2(sb_dbs[idx], sql, -1, &st, 0) != 0 || !st) {
|
|
370
|
+
fclose(destination); unlink(temporary);
|
|
371
|
+
sb_puts(&out, "{\"ok\":false,\"error\":\"cannot read multipart parts\"}");
|
|
372
|
+
return out.p;
|
|
373
|
+
}
|
|
374
|
+
sqlite3_bind_text(st, 1, bucket, -1, SB_SQLITE_TRANSIENT);
|
|
375
|
+
sqlite3_bind_text(st, 2, key, -1, SB_SQLITE_TRANSIENT);
|
|
376
|
+
sqlite3_bind_text(st, 3, upload_id, -1, SB_SQLITE_TRANSIENT);
|
|
377
|
+
|
|
378
|
+
br_sha256_context hash;
|
|
379
|
+
br_sha256_init(&hash);
|
|
380
|
+
size_t total = 0;
|
|
381
|
+
int failed = 0;
|
|
382
|
+
unsigned char chunk[65536];
|
|
383
|
+
while (!failed && sqlite3_step(st) == SB_SQLITE_ROW) {
|
|
384
|
+
int part_number = sqlite3_column_int(st, 0);
|
|
385
|
+
size_t expected = (size_t)sqlite3_column_int64(st, 1);
|
|
386
|
+
char partpath[1024];
|
|
387
|
+
sb_r2_part_path(partpath, sizeof(partpath), path, bucket, key, upload_id, part_number);
|
|
388
|
+
FILE* part = fopen(partpath, "rb");
|
|
389
|
+
if (!part) { failed = 1; break; }
|
|
390
|
+
size_t part_size = 0;
|
|
391
|
+
for (;;) {
|
|
392
|
+
size_t read_count = fread(chunk, 1, sizeof(chunk), part);
|
|
393
|
+
if (read_count) {
|
|
394
|
+
if (fwrite(chunk, 1, read_count, destination) != read_count) { failed = 1; break; }
|
|
395
|
+
br_sha256_update(&hash, chunk, read_count);
|
|
396
|
+
part_size += read_count;
|
|
397
|
+
total += read_count;
|
|
398
|
+
}
|
|
399
|
+
if (read_count < sizeof(chunk)) {
|
|
400
|
+
if (ferror(part)) failed = 1;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
fclose(part);
|
|
405
|
+
if (part_size != expected) failed = 1;
|
|
406
|
+
}
|
|
407
|
+
sqlite3_finalize(st);
|
|
408
|
+
if (fclose(destination) != 0) failed = 1;
|
|
409
|
+
if (failed) {
|
|
410
|
+
unlink(temporary);
|
|
411
|
+
sb_puts(&out, "{\"ok\":false,\"error\":\"multipart completion failed\"}");
|
|
412
|
+
return out.p;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
unsigned char digest[32];
|
|
416
|
+
br_sha256_out(&hash, digest);
|
|
417
|
+
char etag[65];
|
|
418
|
+
sb_hex32(digest, etag);
|
|
419
|
+
char generation_path[1024];
|
|
420
|
+
sb_r2_blob_path(generation_path, sizeof(generation_path), path, bucket, key, etag);
|
|
421
|
+
if (rename(temporary, generation_path) != 0) {
|
|
422
|
+
unlink(temporary);
|
|
423
|
+
sb_puts(&out, "{\"ok\":false,\"error\":\"multipart completion failed\"}");
|
|
424
|
+
return out.p;
|
|
425
|
+
}
|
|
426
|
+
char result[160];
|
|
427
|
+
int n = snprintf(result, sizeof(result), "{\"ok\":true,\"etag\":\"%s\",\"size\":%zu}", etag, total);
|
|
428
|
+
sb_put(&out, result, (size_t)n);
|
|
429
|
+
return out.p;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
static void sb_r2_multipart_delete_parts_c(const char* path, const char* bucket, const char* key, const char* upload_id) {
|
|
433
|
+
int idx = sb_db_for(path);
|
|
434
|
+
if (idx < 0) return;
|
|
435
|
+
sqlite3_stmt* st = 0;
|
|
436
|
+
const char* sql = "SELECT part_number FROM r2_part WHERE bucket = ?1 AND key = ?2 AND upload_id = ?3";
|
|
437
|
+
if (sqlite3_prepare_v2(sb_dbs[idx], sql, -1, &st, 0) != 0 || !st) return;
|
|
438
|
+
sqlite3_bind_text(st, 1, bucket, -1, SB_SQLITE_TRANSIENT);
|
|
439
|
+
sqlite3_bind_text(st, 2, key, -1, SB_SQLITE_TRANSIENT);
|
|
440
|
+
sqlite3_bind_text(st, 3, upload_id, -1, SB_SQLITE_TRANSIENT);
|
|
441
|
+
while (sqlite3_step(st) == SB_SQLITE_ROW) {
|
|
442
|
+
char partpath[1024];
|
|
443
|
+
sb_r2_part_path(partpath, sizeof(partpath), path, bucket, key, upload_id, sqlite3_column_int(st, 0));
|
|
444
|
+
unlink(partpath);
|
|
445
|
+
}
|
|
446
|
+
sqlite3_finalize(st);
|
|
447
|
+
}
|
|
448
|
+
|
|
299
449
|
// Returns malloc'd JSON metadata; the body itself is never serialised into it.
|
|
300
450
|
static char* sb_r2_put_c(const char* path, const char* bucket, const char* key, const char* body, size_t bodylen,
|
|
301
451
|
const char* http_json, const char* custom_json) {
|
|
@@ -309,20 +459,40 @@ static char* sb_r2_put_c(const char* path, const char* bucket, const char* key,
|
|
|
309
459
|
char uploaded[40];
|
|
310
460
|
sb_iso_now(uploaded, sizeof(uploaded));
|
|
311
461
|
|
|
462
|
+
char previous[65];
|
|
463
|
+
previous[0] = 0;
|
|
464
|
+
sqlite3_stmt* old = 0;
|
|
465
|
+
if (sqlite3_prepare_v2(db, "SELECT blob_id FROM r2 WHERE bucket = ?1 AND key = ?2", -1, &old, 0) == 0 && old) {
|
|
466
|
+
sqlite3_bind_text(old, 1, bucket, -1, SB_SQLITE_TRANSIENT);
|
|
467
|
+
sqlite3_bind_text(old, 2, key, -1, SB_SQLITE_TRANSIENT);
|
|
468
|
+
if (sqlite3_step(old) == SB_SQLITE_ROW) {
|
|
469
|
+
const unsigned char* value = sqlite3_column_text(old, 0);
|
|
470
|
+
if (value) snprintf(previous, sizeof(previous), "%s", (const char*)value);
|
|
471
|
+
}
|
|
472
|
+
sqlite3_finalize(old);
|
|
473
|
+
}
|
|
474
|
+
|
|
312
475
|
char blobpath[1024];
|
|
313
|
-
sb_r2_blob_path(blobpath, sizeof(blobpath), path, bucket, key);
|
|
476
|
+
sb_r2_blob_path(blobpath, sizeof(blobpath), path, bucket, key, etag);
|
|
314
477
|
sb_mkdirs(blobpath);
|
|
315
|
-
|
|
478
|
+
char temporary[1100];
|
|
479
|
+
snprintf(temporary, sizeof(temporary), "%s.tmp", blobpath);
|
|
480
|
+
FILE* f = fopen(temporary, "wb");
|
|
316
481
|
if (!f) { sb_puts(&out, "{\"ok\":false,\"error\":\"cannot write object\"}"); return out.p; }
|
|
317
482
|
size_t written = bodylen ? fwrite(body, 1, bodylen, f) : 0;
|
|
318
|
-
fclose(f);
|
|
319
|
-
if (written != bodylen
|
|
483
|
+
int close_rc = fclose(f);
|
|
484
|
+
if (written != bodylen || close_rc != 0 || rename(temporary, blobpath) != 0) {
|
|
485
|
+
unlink(temporary);
|
|
486
|
+
sb_puts(&out, "{\"ok\":false,\"error\":\"object write failed\"}");
|
|
487
|
+
return out.p;
|
|
488
|
+
}
|
|
320
489
|
|
|
321
490
|
sqlite3_stmt* st = 0;
|
|
322
491
|
const char* sql =
|
|
323
|
-
"INSERT INTO r2 (bucket, key, size, etag, uploaded, http_json, custom_json) VALUES (?1,?2,?3,?4,?5,?6,?7) "
|
|
324
|
-
"ON CONFLICT (bucket, key) DO UPDATE SET size=?3, etag=?4, uploaded=?5, http_json=?6, custom_json=?7";
|
|
492
|
+
"INSERT INTO r2 (bucket, key, size, etag, uploaded, http_json, custom_json, blob_id) VALUES (?1,?2,?3,?4,?5,?6,?7,?8) "
|
|
493
|
+
"ON CONFLICT (bucket, key) DO UPDATE SET size=?3, etag=?4, uploaded=?5, http_json=?6, custom_json=?7, blob_id=?8";
|
|
325
494
|
if (sqlite3_prepare_v2(db, sql, -1, &st, 0) != 0 || !st) {
|
|
495
|
+
if (strcmp(previous, etag) != 0) unlink(blobpath);
|
|
326
496
|
sb_puts(&out, "{\"ok\":false,\"error\":");
|
|
327
497
|
const char* m = sqlite3_errmsg(db);
|
|
328
498
|
sb_putjson(&out, m, strlen(m));
|
|
@@ -336,9 +506,23 @@ static char* sb_r2_put_c(const char* path, const char* bucket, const char* key,
|
|
|
336
506
|
sqlite3_bind_text(st, 5, uploaded, -1, SB_SQLITE_TRANSIENT);
|
|
337
507
|
sqlite3_bind_text(st, 6, http_json && *http_json ? http_json : "{}", -1, SB_SQLITE_TRANSIENT);
|
|
338
508
|
sqlite3_bind_text(st, 7, custom_json && *custom_json ? custom_json : "{}", -1, SB_SQLITE_TRANSIENT);
|
|
509
|
+
sqlite3_bind_text(st, 8, etag, -1, SB_SQLITE_TRANSIENT);
|
|
339
510
|
int rc = sqlite3_step(st);
|
|
340
511
|
sqlite3_finalize(st);
|
|
341
|
-
if (rc != SB_SQLITE_DONE) {
|
|
512
|
+
if (rc != SB_SQLITE_DONE) {
|
|
513
|
+
if (strcmp(previous, etag) != 0) unlink(blobpath);
|
|
514
|
+
sb_puts(&out, "{\"ok\":false,\"error\":\"r2 put failed\"}");
|
|
515
|
+
return out.p;
|
|
516
|
+
}
|
|
517
|
+
if (previous[0] && strcmp(previous, etag) != 0) {
|
|
518
|
+
char oldpath[1024];
|
|
519
|
+
sb_r2_blob_path(oldpath, sizeof(oldpath), path, bucket, key, previous);
|
|
520
|
+
unlink(oldpath);
|
|
521
|
+
} else if (!previous[0]) {
|
|
522
|
+
char legacy[1024];
|
|
523
|
+
sb_r2_blob_path(legacy, sizeof(legacy), path, bucket, key, "");
|
|
524
|
+
if (strcmp(legacy, blobpath) != 0) unlink(legacy);
|
|
525
|
+
}
|
|
342
526
|
|
|
343
527
|
char head[256];
|
|
344
528
|
int k = snprintf(head, sizeof(head), "{\"ok\":true,\"etag\":\"%s\",\"size\":%zu,\"uploaded\":\"%s\"}", etag, bodylen, uploaded);
|
|
@@ -350,7 +534,7 @@ static char* sb_r2_put_c(const char* path, const char* bucket, const char* key,
|
|
|
350
534
|
// read) plus the bytestring's own allocation — no SQLite page-cache copy on
|
|
351
535
|
// top, and no growing single database file holding every object ever put.
|
|
352
536
|
// Returns the bytestring pointer, or 0.
|
|
353
|
-
static u32 sb_r2_get_c(const char* path, const char* bucket, const char* key, int* found) {
|
|
537
|
+
static u32 sb_r2_get_c(const char* path, const char* bucket, const char* key, const char* generation, int* found) {
|
|
354
538
|
*found = 0;
|
|
355
539
|
int idx = sb_db_for(path);
|
|
356
540
|
if (idx < 0) return 0;
|
|
@@ -363,7 +547,7 @@ static u32 sb_r2_get_c(const char* path, const char* bucket, const char* key, in
|
|
|
363
547
|
if (!has_row) return 0;
|
|
364
548
|
|
|
365
549
|
char blobpath[1024];
|
|
366
|
-
sb_r2_blob_path(blobpath, sizeof(blobpath), path, bucket, key);
|
|
550
|
+
sb_r2_blob_path(blobpath, sizeof(blobpath), path, bucket, key, generation);
|
|
367
551
|
FILE* f = fopen(blobpath, "rb");
|
|
368
552
|
// A metadata row with no blob file (deleted out from under it, disk issue)
|
|
369
553
|
// reads as not-found rather than crashing.
|
|
@@ -384,12 +568,184 @@ static u32 sb_r2_get_c(const char* path, const char* bucket, const char* key, in
|
|
|
384
568
|
|
|
385
569
|
// Best-effort: a metadata row deleted with no blob file (or vice versa) is
|
|
386
570
|
// still a correct end state, so a missing file here is not an error.
|
|
387
|
-
static void sb_r2_delete_blob(const char* path, const char* bucket, const char* key) {
|
|
571
|
+
static void sb_r2_delete_blob(const char* path, const char* bucket, const char* key, const char* generation) {
|
|
388
572
|
char blobpath[1024];
|
|
389
|
-
sb_r2_blob_path(blobpath, sizeof(blobpath), path, bucket, key);
|
|
573
|
+
sb_r2_blob_path(blobpath, sizeof(blobpath), path, bucket, key, generation);
|
|
390
574
|
unlink(blobpath);
|
|
391
575
|
}
|
|
392
576
|
|
|
577
|
+
// The native-fetch C++ shim calls this narrow ABI for a reserved R2 transfer
|
|
578
|
+
// path before it allocates a PendingRequest body. The context owns only a file
|
|
579
|
+
// descriptor and small metadata buffers; object bytes stay in the kernel page
|
|
580
|
+
// cache until the final fixed-size digest pass.
|
|
581
|
+
typedef struct sb_r2_transfer_ctx {
|
|
582
|
+
FILE* file;
|
|
583
|
+
sqlite3* db;
|
|
584
|
+
char store[1024], bucket[256], key[512], token[32], expected[65], http[1024], custom[1024], temporary[1200];
|
|
585
|
+
size_t size, max_bytes;
|
|
586
|
+
} sb_r2_transfer_ctx;
|
|
587
|
+
|
|
588
|
+
extern const char* sb_standalone_app_name(void);
|
|
589
|
+
|
|
590
|
+
static void sb_r2_transfer_store_path(char* out, size_t cap) {
|
|
591
|
+
const char* dir = getenv("SB_DATA_DIR");
|
|
592
|
+
if (!dir || !*dir) dir = getenv("SPROUTBOAT_DATA");
|
|
593
|
+
if (!dir || !*dir) {
|
|
594
|
+
const char* app = sb_standalone_app_name();
|
|
595
|
+
snprintf(out, cap, "%s.data/store.sqlite", app && *app ? app : "app");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
snprintf(out, cap, "%s/store.sqlite", dir);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Return HTTP status, or 0 when a one-use upload ticket was claimed.
|
|
602
|
+
int sb_r2_transfer_open(const char* bucket, const char* token, size_t declared, sb_r2_transfer_ctx** out) {
|
|
603
|
+
*out = 0;
|
|
604
|
+
if (!bucket || !token || strlen(bucket) >= 256 || strlen(token) != 24) return 404;
|
|
605
|
+
sb_r2_transfer_ctx* ctx = (sb_r2_transfer_ctx*)calloc(1, sizeof(*ctx));
|
|
606
|
+
if (!ctx) return 503;
|
|
607
|
+
sb_r2_transfer_store_path(ctx->store, sizeof(ctx->store));
|
|
608
|
+
int index = sb_db_for(ctx->store);
|
|
609
|
+
if (index < 0) { free(ctx); return 503; }
|
|
610
|
+
ctx->db = sb_dbs[index];
|
|
611
|
+
sqlite3_stmt* st = 0;
|
|
612
|
+
const char* sql = "SELECT key, max_bytes, expected_sha256, http_json, custom_json FROM r2_transfer "
|
|
613
|
+
"WHERE token=?1 AND bucket=?2 AND method='upload' AND used=0 AND expires>=?3";
|
|
614
|
+
if (sqlite3_prepare_v2(ctx->db, sql, -1, &st, 0) != 0 || !st) { free(ctx); return 503; }
|
|
615
|
+
sqlite3_bind_text(st, 1, token, -1, SB_SQLITE_TRANSIENT);
|
|
616
|
+
sqlite3_bind_text(st, 2, bucket, -1, SB_SQLITE_TRANSIENT);
|
|
617
|
+
sqlite3_bind_int64(st, 3, (int64_t)time(0) * 1000);
|
|
618
|
+
if (sqlite3_step(st) != SB_SQLITE_ROW) { sqlite3_finalize(st); free(ctx); return 410; }
|
|
619
|
+
const char* key = (const char*)sqlite3_column_text(st, 0);
|
|
620
|
+
ctx->max_bytes = (size_t)sqlite3_column_int64(st, 1);
|
|
621
|
+
const char* expected = (const char*)sqlite3_column_text(st, 2);
|
|
622
|
+
const char* http = (const char*)sqlite3_column_text(st, 3);
|
|
623
|
+
const char* custom = (const char*)sqlite3_column_text(st, 4);
|
|
624
|
+
if ((key && strlen(key) >= sizeof(ctx->key)) ||
|
|
625
|
+
(expected && strlen(expected) >= sizeof(ctx->expected)) ||
|
|
626
|
+
(http && strlen(http) >= sizeof(ctx->http)) ||
|
|
627
|
+
(custom && strlen(custom) >= sizeof(ctx->custom))) {
|
|
628
|
+
sqlite3_finalize(st); free(ctx); return 413;
|
|
629
|
+
}
|
|
630
|
+
snprintf(ctx->bucket, sizeof(ctx->bucket), "%s", bucket);
|
|
631
|
+
snprintf(ctx->key, sizeof(ctx->key), "%s", key ? key : "");
|
|
632
|
+
snprintf(ctx->token, sizeof(ctx->token), "%s", token);
|
|
633
|
+
snprintf(ctx->expected, sizeof(ctx->expected), "%s", expected ? expected : "");
|
|
634
|
+
snprintf(ctx->http, sizeof(ctx->http), "%s", http ? http : "{}");
|
|
635
|
+
snprintf(ctx->custom, sizeof(ctx->custom), "%s", custom ? custom : "{}");
|
|
636
|
+
sqlite3_finalize(st);
|
|
637
|
+
if (declared > ctx->max_bytes) { free(ctx); return 413; }
|
|
638
|
+
st = 0;
|
|
639
|
+
if (sqlite3_prepare_v2(ctx->db, "UPDATE r2_transfer SET used=1 WHERE token=?1 AND used=0", -1, &st, 0) != 0 || !st) { free(ctx); return 503; }
|
|
640
|
+
sqlite3_bind_text(st, 1, token, -1, SB_SQLITE_TRANSIENT);
|
|
641
|
+
int claimed = sqlite3_step(st) == SB_SQLITE_DONE && sqlite3_changes(ctx->db) == 1;
|
|
642
|
+
sqlite3_finalize(st);
|
|
643
|
+
if (!claimed) { free(ctx); return 410; }
|
|
644
|
+
const char* slash = strrchr(ctx->store, '/');
|
|
645
|
+
if (!slash) { free(ctx); return 500; }
|
|
646
|
+
char blobdir[1024];
|
|
647
|
+
sb_r2_blob_path(blobdir, sizeof(blobdir), ctx->store, ctx->bucket, ctx->key, "");
|
|
648
|
+
sb_mkdirs(blobdir);
|
|
649
|
+
if (snprintf(ctx->temporary, sizeof(ctx->temporary), "%.*s/r2-blobs/%s.upload.tmp", (int)(slash - ctx->store), ctx->store, token) >= (int)sizeof(ctx->temporary)) { free(ctx); return 507; }
|
|
650
|
+
ctx->file = fopen(ctx->temporary, "wb");
|
|
651
|
+
if (!ctx->file) { free(ctx); return 507; }
|
|
652
|
+
*out = ctx;
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
int sb_r2_transfer_write(sb_r2_transfer_ctx* ctx, const char* bytes, size_t length) {
|
|
657
|
+
if (!ctx || !ctx->file || length > ctx->max_bytes - ctx->size) return 413;
|
|
658
|
+
if (length && fwrite(bytes, 1, length, ctx->file) != length) return 507;
|
|
659
|
+
ctx->size += length;
|
|
660
|
+
return 0;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Finish the native direct-upload path. The digest is deliberately computed
|
|
664
|
+
// from the temporary file in fixed-size chunks so neither validation nor the
|
|
665
|
+
// immutable-generation commit ever materialises the object in process memory.
|
|
666
|
+
int sb_r2_transfer_finish(sb_r2_transfer_ctx* ctx) {
|
|
667
|
+
if (!ctx || !ctx->file) return 500;
|
|
668
|
+
if (fclose(ctx->file) != 0) { ctx->file = 0; unlink(ctx->temporary); free(ctx); return 507; }
|
|
669
|
+
ctx->file = 0;
|
|
670
|
+
|
|
671
|
+
FILE* source = fopen(ctx->temporary, "rb");
|
|
672
|
+
if (!source) { unlink(ctx->temporary); free(ctx); return 500; }
|
|
673
|
+
br_sha256_context hash;
|
|
674
|
+
br_sha256_init(&hash);
|
|
675
|
+
unsigned char chunk[65536];
|
|
676
|
+
for (;;) {
|
|
677
|
+
size_t count = fread(chunk, 1, sizeof(chunk), source);
|
|
678
|
+
if (count) br_sha256_update(&hash, chunk, count);
|
|
679
|
+
if (count < sizeof(chunk)) {
|
|
680
|
+
if (ferror(source)) { fclose(source); unlink(ctx->temporary); free(ctx); return 500; }
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
fclose(source);
|
|
685
|
+
unsigned char digest[32];
|
|
686
|
+
char etag[65];
|
|
687
|
+
br_sha256_out(&hash, digest);
|
|
688
|
+
sb_hex32(digest, etag);
|
|
689
|
+
if (ctx->expected[0] && strcmp(ctx->expected, etag) != 0) {
|
|
690
|
+
unlink(ctx->temporary); free(ctx); return 422;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
char previous[65] = { 0 };
|
|
694
|
+
sqlite3_stmt* old = 0;
|
|
695
|
+
if (sqlite3_prepare_v2(ctx->db, "SELECT blob_id FROM r2 WHERE bucket=?1 AND key=?2", -1, &old, 0) == 0 && old) {
|
|
696
|
+
sqlite3_bind_text(old, 1, ctx->bucket, -1, SB_SQLITE_TRANSIENT);
|
|
697
|
+
sqlite3_bind_text(old, 2, ctx->key, -1, SB_SQLITE_TRANSIENT);
|
|
698
|
+
if (sqlite3_step(old) == SB_SQLITE_ROW) {
|
|
699
|
+
const unsigned char* value = sqlite3_column_text(old, 0);
|
|
700
|
+
if (value) snprintf(previous, sizeof(previous), "%s", (const char*)value);
|
|
701
|
+
}
|
|
702
|
+
sqlite3_finalize(old);
|
|
703
|
+
}
|
|
704
|
+
char blobpath[1024];
|
|
705
|
+
sb_r2_blob_path(blobpath, sizeof(blobpath), ctx->store, ctx->bucket, ctx->key, etag);
|
|
706
|
+
sb_mkdirs(blobpath);
|
|
707
|
+
if (rename(ctx->temporary, blobpath) != 0) { unlink(ctx->temporary); free(ctx); return 507; }
|
|
708
|
+
|
|
709
|
+
char uploaded[40];
|
|
710
|
+
sb_iso_now(uploaded, sizeof(uploaded));
|
|
711
|
+
sqlite3_stmt* st = 0;
|
|
712
|
+
const char* sql =
|
|
713
|
+
"INSERT INTO r2 (bucket,key,size,etag,uploaded,http_json,custom_json,blob_id) VALUES (?1,?2,?3,?4,?5,?6,?7,?8) "
|
|
714
|
+
"ON CONFLICT(bucket,key) DO UPDATE SET size=?3,etag=?4,uploaded=?5,http_json=?6,custom_json=?7,blob_id=?8";
|
|
715
|
+
if (sqlite3_prepare_v2(ctx->db, sql, -1, &st, 0) != 0 || !st) {
|
|
716
|
+
if (strcmp(previous, etag) != 0) unlink(blobpath);
|
|
717
|
+
free(ctx); return 500;
|
|
718
|
+
}
|
|
719
|
+
sqlite3_bind_text(st, 1, ctx->bucket, -1, SB_SQLITE_TRANSIENT);
|
|
720
|
+
sqlite3_bind_text(st, 2, ctx->key, -1, SB_SQLITE_TRANSIENT);
|
|
721
|
+
sqlite3_bind_int64(st, 3, (int64_t)ctx->size);
|
|
722
|
+
sqlite3_bind_text(st, 4, etag, -1, SB_SQLITE_TRANSIENT);
|
|
723
|
+
sqlite3_bind_text(st, 5, uploaded, -1, SB_SQLITE_TRANSIENT);
|
|
724
|
+
sqlite3_bind_text(st, 6, ctx->http[0] ? ctx->http : "{}", -1, SB_SQLITE_TRANSIENT);
|
|
725
|
+
sqlite3_bind_text(st, 7, ctx->custom[0] ? ctx->custom : "{}", -1, SB_SQLITE_TRANSIENT);
|
|
726
|
+
sqlite3_bind_text(st, 8, etag, -1, SB_SQLITE_TRANSIENT);
|
|
727
|
+
int rc = sqlite3_step(st);
|
|
728
|
+
sqlite3_finalize(st);
|
|
729
|
+
if (rc != SB_SQLITE_DONE) {
|
|
730
|
+
if (strcmp(previous, etag) != 0) unlink(blobpath);
|
|
731
|
+
free(ctx); return 500;
|
|
732
|
+
}
|
|
733
|
+
if (previous[0] && strcmp(previous, etag) != 0) {
|
|
734
|
+
char oldpath[1024];
|
|
735
|
+
sb_r2_blob_path(oldpath, sizeof(oldpath), ctx->store, ctx->bucket, ctx->key, previous);
|
|
736
|
+
unlink(oldpath);
|
|
737
|
+
}
|
|
738
|
+
free(ctx);
|
|
739
|
+
return 201;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
void sb_r2_transfer_abort(sb_r2_transfer_ctx* ctx) {
|
|
743
|
+
if (!ctx) return;
|
|
744
|
+
if (ctx->file) fclose(ctx->file);
|
|
745
|
+
if (ctx->temporary[0]) unlink(ctx->temporary);
|
|
746
|
+
free(ctx);
|
|
747
|
+
}
|
|
748
|
+
|
|
393
749
|
// Run a script: one or more statements separated by semicolons. sqlite3_exec
|
|
394
750
|
// handles the whole string, which prepare/step does not — it compiles the first
|
|
395
751
|
// statement and silently ignores the rest, so a schema built from one exec call
|
|
@@ -1008,9 +1364,114 @@ function __sbR2PutRaw(path, bucket, key, body, httpJson, customJson) {
|
|
|
1008
1364
|
return res;
|
|
1009
1365
|
}
|
|
1010
1366
|
|
|
1367
|
+
// Multipart parts are persisted individually. Completion streams them through
|
|
1368
|
+
// a fixed 64 KiB C buffer, so the standalone heap never holds the whole object.
|
|
1369
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
1370
|
+
function __sbR2MultipartPutRaw(path, bucket, key, uploadId, partNumberText, body) {
|
|
1371
|
+
let res = "";
|
|
1372
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
1373
|
+
Porffor.c`
|
|
1374
|
+
const char* __p; size_t __pl; char* __po = 0;
|
|
1375
|
+
porf_native_fetch_read_value(path, &__p, &__pl, &__po);
|
|
1376
|
+
char* __path = (char*)malloc(__pl + 1); memcpy(__path, __p, __pl); __path[__pl] = 0;
|
|
1377
|
+
if (__po) free(__po);
|
|
1378
|
+
|
|
1379
|
+
const char* __bk; size_t __bkl; char* __bko = 0;
|
|
1380
|
+
porf_native_fetch_read_value(bucket, &__bk, &__bkl, &__bko);
|
|
1381
|
+
char* __bucket = (char*)malloc(__bkl + 1); memcpy(__bucket, __bk, __bkl); __bucket[__bkl] = 0;
|
|
1382
|
+
if (__bko) free(__bko);
|
|
1383
|
+
|
|
1384
|
+
const char* __k; size_t __kl; char* __ko = 0;
|
|
1385
|
+
porf_native_fetch_read_value(key, &__k, &__kl, &__ko);
|
|
1386
|
+
char* __key = (char*)malloc(__kl + 1); memcpy(__key, __k, __kl); __key[__kl] = 0;
|
|
1387
|
+
if (__ko) free(__ko);
|
|
1388
|
+
|
|
1389
|
+
const char* __u; size_t __ul; char* __uo = 0;
|
|
1390
|
+
porf_native_fetch_read_value(uploadId, &__u, &__ul, &__uo);
|
|
1391
|
+
char* __upload = (char*)malloc(__ul + 1); memcpy(__upload, __u, __ul); __upload[__ul] = 0;
|
|
1392
|
+
if (__uo) free(__uo);
|
|
1393
|
+
|
|
1394
|
+
const char* __pn; size_t __pnl; char* __pno = 0;
|
|
1395
|
+
porf_native_fetch_read_value(partNumberText, &__pn, &__pnl, &__pno);
|
|
1396
|
+
char __partbuf[16];
|
|
1397
|
+
size_t __partlen = __pnl < sizeof(__partbuf) - 1 ? __pnl : sizeof(__partbuf) - 1;
|
|
1398
|
+
memcpy(__partbuf, __pn, __partlen); __partbuf[__partlen] = 0;
|
|
1399
|
+
if (__pno) free(__pno);
|
|
1400
|
+
|
|
1401
|
+
const char* __b; size_t __bl; char* __bo = 0;
|
|
1402
|
+
if (porf_native_fetch_read_raw_bytes(body, &__b, &__bl) != 0) {
|
|
1403
|
+
porf_native_fetch_read_value(body, &__b, &__bl, &__bo);
|
|
1404
|
+
}
|
|
1405
|
+
char* __out = sb_r2_part_put_c(__path, __bucket, __key, __upload, atoi(__partbuf), __b, __bl);
|
|
1406
|
+
if (__bo) free(__bo);
|
|
1407
|
+
free(__path); free(__bucket); free(__key); free(__upload);
|
|
1408
|
+
if (__out) {
|
|
1409
|
+
res = porf_box((f64)porf_native_fetch_alloc_bytestring(__out, strlen(__out)), 195);
|
|
1410
|
+
free(__out);
|
|
1411
|
+
}
|
|
1412
|
+
`;
|
|
1413
|
+
return res;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
1417
|
+
function __sbR2MultipartCompleteRaw(path, bucket, key, uploadId) {
|
|
1418
|
+
let res = "";
|
|
1419
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
1420
|
+
Porffor.c`
|
|
1421
|
+
const char* __p; size_t __pl; char* __po = 0;
|
|
1422
|
+
porf_native_fetch_read_value(path, &__p, &__pl, &__po);
|
|
1423
|
+
char* __path = (char*)malloc(__pl + 1); memcpy(__path, __p, __pl); __path[__pl] = 0;
|
|
1424
|
+
if (__po) free(__po);
|
|
1425
|
+
const char* __bk; size_t __bkl; char* __bko = 0;
|
|
1426
|
+
porf_native_fetch_read_value(bucket, &__bk, &__bkl, &__bko);
|
|
1427
|
+
char* __bucket = (char*)malloc(__bkl + 1); memcpy(__bucket, __bk, __bkl); __bucket[__bkl] = 0;
|
|
1428
|
+
if (__bko) free(__bko);
|
|
1429
|
+
const char* __k; size_t __kl; char* __ko = 0;
|
|
1430
|
+
porf_native_fetch_read_value(key, &__k, &__kl, &__ko);
|
|
1431
|
+
char* __key = (char*)malloc(__kl + 1); memcpy(__key, __k, __kl); __key[__kl] = 0;
|
|
1432
|
+
if (__ko) free(__ko);
|
|
1433
|
+
const char* __u; size_t __ul; char* __uo = 0;
|
|
1434
|
+
porf_native_fetch_read_value(uploadId, &__u, &__ul, &__uo);
|
|
1435
|
+
char* __upload = (char*)malloc(__ul + 1); memcpy(__upload, __u, __ul); __upload[__ul] = 0;
|
|
1436
|
+
if (__uo) free(__uo);
|
|
1437
|
+
char* __out = sb_r2_multipart_complete_c(__path, __bucket, __key, __upload);
|
|
1438
|
+
free(__path); free(__bucket); free(__key); free(__upload);
|
|
1439
|
+
if (__out) {
|
|
1440
|
+
res = porf_box((f64)porf_native_fetch_alloc_bytestring(__out, strlen(__out)), 195);
|
|
1441
|
+
free(__out);
|
|
1442
|
+
}
|
|
1443
|
+
`;
|
|
1444
|
+
return res;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
1448
|
+
function __sbR2MultipartDeleteRaw(path, bucket, key, uploadId) {
|
|
1449
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
1450
|
+
Porffor.c`
|
|
1451
|
+
const char* __p; size_t __pl; char* __po = 0;
|
|
1452
|
+
porf_native_fetch_read_value(path, &__p, &__pl, &__po);
|
|
1453
|
+
char* __path = (char*)malloc(__pl + 1); memcpy(__path, __p, __pl); __path[__pl] = 0;
|
|
1454
|
+
if (__po) free(__po);
|
|
1455
|
+
const char* __bk; size_t __bkl; char* __bko = 0;
|
|
1456
|
+
porf_native_fetch_read_value(bucket, &__bk, &__bkl, &__bko);
|
|
1457
|
+
char* __bucket = (char*)malloc(__bkl + 1); memcpy(__bucket, __bk, __bkl); __bucket[__bkl] = 0;
|
|
1458
|
+
if (__bko) free(__bko);
|
|
1459
|
+
const char* __k; size_t __kl; char* __ko = 0;
|
|
1460
|
+
porf_native_fetch_read_value(key, &__k, &__kl, &__ko);
|
|
1461
|
+
char* __key = (char*)malloc(__kl + 1); memcpy(__key, __k, __kl); __key[__kl] = 0;
|
|
1462
|
+
if (__ko) free(__ko);
|
|
1463
|
+
const char* __u; size_t __ul; char* __uo = 0;
|
|
1464
|
+
porf_native_fetch_read_value(uploadId, &__u, &__ul, &__uo);
|
|
1465
|
+
char* __upload = (char*)malloc(__ul + 1); memcpy(__upload, __u, __ul); __upload[__ul] = 0;
|
|
1466
|
+
if (__uo) free(__uo);
|
|
1467
|
+
sb_r2_multipart_delete_parts_c(__path, __bucket, __key, __upload);
|
|
1468
|
+
free(__path); free(__bucket); free(__key); free(__upload);
|
|
1469
|
+
`;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1011
1472
|
// R2 get: the bytes come back as a bytestring, not inside a reply frame.
|
|
1012
1473
|
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
1013
|
-
function __sbR2GetRaw(path, bucket, key) {
|
|
1474
|
+
function __sbR2GetRaw(path, bucket, key, blobId) {
|
|
1014
1475
|
let res = "";
|
|
1015
1476
|
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
1016
1477
|
Porffor.c`
|
|
@@ -1029,9 +1490,14 @@ function __sbR2GetRaw(path, bucket, key) {
|
|
|
1029
1490
|
char* __key = (char*)malloc(__kl + 1); memcpy(__key, __k, __kl); __key[__kl] = 0;
|
|
1030
1491
|
if (__ko) free(__ko);
|
|
1031
1492
|
|
|
1493
|
+
const char* __g; size_t __gl; char* __go = 0;
|
|
1494
|
+
porf_native_fetch_read_value(blobId, &__g, &__gl, &__go);
|
|
1495
|
+
char* __generation = (char*)malloc(__gl + 1); memcpy(__generation, __g, __gl); __generation[__gl] = 0;
|
|
1496
|
+
if (__go) free(__go);
|
|
1497
|
+
|
|
1032
1498
|
int __found = 0;
|
|
1033
|
-
u32 __bs = sb_r2_get_c(__path, __bucket, __key, &__found);
|
|
1034
|
-
free(__path); free(__bucket); free(__key);
|
|
1499
|
+
u32 __bs = sb_r2_get_c(__path, __bucket, __key, __generation, &__found);
|
|
1500
|
+
free(__path); free(__bucket); free(__key); free(__generation);
|
|
1035
1501
|
if (__found) res = porf_box((f64)__bs, 195);
|
|
1036
1502
|
`;
|
|
1037
1503
|
return res;
|
|
@@ -1040,7 +1506,7 @@ function __sbR2GetRaw(path, bucket, key) {
|
|
|
1040
1506
|
// R2 delete: also removes the object's own file, not just the metadata row
|
|
1041
1507
|
// (#56 — sb_r2_delete_blob is best-effort, a missing file is not an error).
|
|
1042
1508
|
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
1043
|
-
function __sbR2DeleteRaw(path, bucket, key) {
|
|
1509
|
+
function __sbR2DeleteRaw(path, bucket, key, blobId) {
|
|
1044
1510
|
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
1045
1511
|
Porffor.c`
|
|
1046
1512
|
const char* __p; size_t __pl; char* __po = 0;
|
|
@@ -1058,8 +1524,13 @@ function __sbR2DeleteRaw(path, bucket, key) {
|
|
|
1058
1524
|
char* __key = (char*)malloc(__kl + 1); memcpy(__key, __k, __kl); __key[__kl] = 0;
|
|
1059
1525
|
if (__ko) free(__ko);
|
|
1060
1526
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1527
|
+
const char* __g; size_t __gl; char* __go = 0;
|
|
1528
|
+
porf_native_fetch_read_value(blobId, &__g, &__gl, &__go);
|
|
1529
|
+
char* __generation = (char*)malloc(__gl + 1); memcpy(__generation, __g, __gl); __generation[__gl] = 0;
|
|
1530
|
+
if (__go) free(__go);
|
|
1531
|
+
|
|
1532
|
+
sb_r2_delete_blob(__path, __bucket, __key, __generation);
|
|
1533
|
+
free(__path); free(__bucket); free(__key); free(__generation);
|
|
1063
1534
|
`;
|
|
1064
1535
|
}
|
|
1065
1536
|
|
|
@@ -1221,7 +1692,10 @@ function __sbDir() {
|
|
|
1221
1692
|
// Porffor's runtime init calls porf_init(0, NULL). Resolved once — the answer
|
|
1222
1693
|
// cannot change mid-run.
|
|
1223
1694
|
if (!__sbDataDir) {
|
|
1224
|
-
__sbDataDir =
|
|
1695
|
+
__sbDataDir =
|
|
1696
|
+
__sbEnv("SB_DATA_DIR") ||
|
|
1697
|
+
__sbEnv("SPROUTBOAT_DATA") ||
|
|
1698
|
+
(globalThis.__sbAppName || "app") + ".data";
|
|
1225
1699
|
}
|
|
1226
1700
|
return __sbDataDir;
|
|
1227
1701
|
}
|
|
@@ -1253,8 +1727,44 @@ function __sbEnsureSchema() {
|
|
|
1253
1727
|
s,
|
|
1254
1728
|
"CREATE TABLE IF NOT EXISTS r2 (bucket TEXT NOT NULL, key TEXT NOT NULL, size INTEGER NOT NULL, " +
|
|
1255
1729
|
"etag TEXT NOT NULL, uploaded TEXT NOT NULL, http_json TEXT NOT NULL DEFAULT '{}', custom_json TEXT NOT NULL DEFAULT '{}', " +
|
|
1730
|
+
"blob_id TEXT, " +
|
|
1256
1731
|
"PRIMARY KEY (bucket, key))",
|
|
1257
1732
|
);
|
|
1733
|
+
const r2Columns = __sbSql(s, "PRAGMA table_info(r2)", []).rows;
|
|
1734
|
+
let hasBlobId = false;
|
|
1735
|
+
for (let i = 0; i < r2Columns.length; i++) if (r2Columns[i][1] === "blob_id") hasBlobId = true;
|
|
1736
|
+
if (!hasBlobId) __sbSql(s, "ALTER TABLE r2 ADD COLUMN blob_id TEXT", []);
|
|
1737
|
+
__sbSql(
|
|
1738
|
+
s,
|
|
1739
|
+
"CREATE TABLE IF NOT EXISTS r2_upload (bucket TEXT NOT NULL, key TEXT NOT NULL, upload_id TEXT NOT NULL, " +
|
|
1740
|
+
"uploaded TEXT NOT NULL, http_json TEXT NOT NULL DEFAULT '{}', custom_json TEXT NOT NULL DEFAULT '{}', " +
|
|
1741
|
+
"PRIMARY KEY (bucket, key, upload_id))",
|
|
1742
|
+
);
|
|
1743
|
+
__sbSql(
|
|
1744
|
+
s,
|
|
1745
|
+
"CREATE TABLE IF NOT EXISTS r2_part (bucket TEXT NOT NULL, key TEXT NOT NULL, upload_id TEXT NOT NULL, " +
|
|
1746
|
+
"part_number INTEGER NOT NULL, size INTEGER NOT NULL, etag TEXT NOT NULL, " +
|
|
1747
|
+
"PRIMARY KEY (bucket, key, upload_id, part_number))",
|
|
1748
|
+
);
|
|
1749
|
+
__sbSql(
|
|
1750
|
+
s,
|
|
1751
|
+
"CREATE TABLE IF NOT EXISTS r2_transfer (token TEXT PRIMARY KEY, bucket TEXT NOT NULL, key TEXT NOT NULL, " +
|
|
1752
|
+
"method TEXT NOT NULL, expires INTEGER NOT NULL, max_bytes INTEGER NOT NULL, expected_sha256 TEXT NOT NULL DEFAULT '', " +
|
|
1753
|
+
"http_json TEXT NOT NULL DEFAULT '{}', custom_json TEXT NOT NULL DEFAULT '{}', used INTEGER NOT NULL DEFAULT 0)",
|
|
1754
|
+
);
|
|
1755
|
+
const expired = __sbSql(s, "SELECT bucket, key, upload_id FROM r2_upload WHERE uploaded < ?", [
|
|
1756
|
+
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
1757
|
+
]).rows;
|
|
1758
|
+
for (let i = 0; i < expired.length; i++) {
|
|
1759
|
+
__sbR2MultipartDeleteRaw(
|
|
1760
|
+
s,
|
|
1761
|
+
String(expired[i][0]),
|
|
1762
|
+
String(expired[i][1]),
|
|
1763
|
+
String(expired[i][2]),
|
|
1764
|
+
);
|
|
1765
|
+
__sbSql(s, "DELETE FROM r2_part WHERE bucket = ? AND key = ? AND upload_id = ?", expired[i]);
|
|
1766
|
+
__sbSql(s, "DELETE FROM r2_upload WHERE bucket = ? AND key = ? AND upload_id = ?", expired[i]);
|
|
1767
|
+
}
|
|
1258
1768
|
__sbSql(
|
|
1259
1769
|
s,
|
|
1260
1770
|
"CREATE TABLE IF NOT EXISTS mq (queue TEXT NOT NULL, id TEXT PRIMARY KEY, body TEXT NOT NULL, " +
|
|
@@ -1300,14 +1810,16 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1300
1810
|
|
|
1301
1811
|
if (op === "kv.get") {
|
|
1302
1812
|
const r = __sbSql(store, "SELECT value FROM kv WHERE ns = ? AND key = ?", [msg.ns, msg.key]);
|
|
1303
|
-
return r.rows.length
|
|
1813
|
+
return r.rows.length
|
|
1814
|
+
? { ok: true, found: true, value: r.rows[0][0] }
|
|
1815
|
+
: { ok: true, found: false, value: null };
|
|
1304
1816
|
}
|
|
1305
1817
|
if (op === "kv.put") {
|
|
1306
|
-
__sbSql(
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
msg.value,
|
|
1310
|
-
|
|
1818
|
+
__sbSql(
|
|
1819
|
+
store,
|
|
1820
|
+
"INSERT INTO kv (ns, key, value) VALUES (?1,?2,?3) ON CONFLICT (ns, key) DO UPDATE SET value = ?3",
|
|
1821
|
+
[msg.ns, msg.key, msg.value],
|
|
1822
|
+
);
|
|
1311
1823
|
return { ok: true };
|
|
1312
1824
|
}
|
|
1313
1825
|
if (op === "kv.delete") {
|
|
@@ -1350,12 +1862,26 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1350
1862
|
}
|
|
1351
1863
|
if (key != null) {
|
|
1352
1864
|
const info = meta[key] || {};
|
|
1353
|
-
return {
|
|
1865
|
+
return {
|
|
1866
|
+
ok: true,
|
|
1867
|
+
found: true,
|
|
1868
|
+
status: 200,
|
|
1869
|
+
type: info.type,
|
|
1870
|
+
hash: info.hash,
|
|
1871
|
+
body: files[key],
|
|
1872
|
+
};
|
|
1354
1873
|
}
|
|
1355
1874
|
const nfh = (bundle.manifest && bundle.manifest.notFound) || "none";
|
|
1356
1875
|
if (nfh === "single-page-application" && files["/index.html"] != null) {
|
|
1357
1876
|
const info = meta["/index.html"] || {};
|
|
1358
|
-
return {
|
|
1877
|
+
return {
|
|
1878
|
+
ok: true,
|
|
1879
|
+
found: true,
|
|
1880
|
+
status: 200,
|
|
1881
|
+
type: info.type,
|
|
1882
|
+
hash: info.hash,
|
|
1883
|
+
body: files["/index.html"],
|
|
1884
|
+
};
|
|
1359
1885
|
}
|
|
1360
1886
|
if (nfh === "404-page" && files["/404.html"] != null) {
|
|
1361
1887
|
const info = meta["/404.html"] || {};
|
|
@@ -1369,7 +1895,8 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1369
1895
|
const tls = url.protocol === "https:";
|
|
1370
1896
|
if (!tls && url.protocol !== "http:") throw new Error("unsupported protocol: " + url.protocol);
|
|
1371
1897
|
const allow = bindingsOutbound();
|
|
1372
|
-
if (allow.indexOf(url.host) === -1)
|
|
1898
|
+
if (allow.indexOf(url.host) === -1)
|
|
1899
|
+
throw new Error("host not in outbound allowlist: " + url.host);
|
|
1373
1900
|
let headerText = "";
|
|
1374
1901
|
const pairs = msg.headers || [];
|
|
1375
1902
|
for (let i = 0; i < pairs.length; i++) {
|
|
@@ -1420,7 +1947,8 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1420
1947
|
if (op === "d1.batch") {
|
|
1421
1948
|
const results = [];
|
|
1422
1949
|
const list = msg.statements || [];
|
|
1423
|
-
for (let i = 0; i < list.length; i++)
|
|
1950
|
+
for (let i = 0; i < list.length; i++)
|
|
1951
|
+
results.push(__sbD1Run(path, list[i].sql, list[i].params));
|
|
1424
1952
|
return { ok: true, results };
|
|
1425
1953
|
}
|
|
1426
1954
|
const one = __sbD1Run(path, msg.sql, msg.params);
|
|
@@ -1459,7 +1987,175 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1459
1987
|
),
|
|
1460
1988
|
);
|
|
1461
1989
|
if (reply.ok === false) throw new Error("r2 put: " + reply.error);
|
|
1462
|
-
return {
|
|
1990
|
+
return {
|
|
1991
|
+
ok: true,
|
|
1992
|
+
object: { key: msg.key, size: reply.size, etag: reply.etag, uploaded: reply.uploaded },
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
if (op === "r2.transfer.create") {
|
|
1996
|
+
const method = msg.method === "upload" || msg.method === "download" ? msg.method : "";
|
|
1997
|
+
if (!method) throw new Error("transfer method must be upload or download");
|
|
1998
|
+
const expiresIn = Math.min(Math.max(Number(msg.expiresIn) || 900, 30), 3600);
|
|
1999
|
+
const maxBytes =
|
|
2000
|
+
method === "upload"
|
|
2001
|
+
? Math.min(Math.max(Number(msg.maxBytes) || 100 * 1024 * 1024, 1), 5 * 1024 * 1024 * 1024)
|
|
2002
|
+
: 0;
|
|
2003
|
+
const expectedSha256 = String(msg.sha256 || "").toLowerCase();
|
|
2004
|
+
if (expectedSha256 && !/^[0-9a-f]{64}$/.test(expectedSha256))
|
|
2005
|
+
throw new Error("sha256 must be 64 hexadecimal characters");
|
|
2006
|
+
const expires = Date.now() + expiresIn * 1000;
|
|
2007
|
+
__sbSql(store, "DELETE FROM r2_transfer WHERE expires < ? OR used = 1", [Date.now()]);
|
|
2008
|
+
const token = __sbHex(12);
|
|
2009
|
+
__sbSql(
|
|
2010
|
+
store,
|
|
2011
|
+
"INSERT INTO r2_transfer (token, bucket, key, method, expires, max_bytes, expected_sha256, http_json, custom_json) " +
|
|
2012
|
+
"VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
|
|
2013
|
+
[
|
|
2014
|
+
token,
|
|
2015
|
+
msg.bucket,
|
|
2016
|
+
msg.key,
|
|
2017
|
+
method,
|
|
2018
|
+
expires,
|
|
2019
|
+
maxBytes,
|
|
2020
|
+
expectedSha256,
|
|
2021
|
+
JSON.stringify(msg.httpMetadata || {}),
|
|
2022
|
+
JSON.stringify(msg.customMetadata || {}),
|
|
2023
|
+
],
|
|
2024
|
+
);
|
|
2025
|
+
return {
|
|
2026
|
+
ok: true,
|
|
2027
|
+
url: "/__sb/r2/transfer/" + encodeURIComponent(String(msg.bucket)) + "/" + token,
|
|
2028
|
+
expiresAt: new Date(expires).toISOString(),
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
if (op === "r2.multipart.create") {
|
|
2032
|
+
const uploadId = String(msg.uploadId || __sbHex(16));
|
|
2033
|
+
__sbSql(
|
|
2034
|
+
store,
|
|
2035
|
+
"INSERT INTO r2_upload (bucket, key, upload_id, uploaded, http_json, custom_json) VALUES (?1,?2,?3,?4,?5,?6) " +
|
|
2036
|
+
"ON CONFLICT (bucket, key, upload_id) DO NOTHING",
|
|
2037
|
+
[
|
|
2038
|
+
msg.bucket,
|
|
2039
|
+
msg.key,
|
|
2040
|
+
uploadId,
|
|
2041
|
+
new Date().toISOString(),
|
|
2042
|
+
JSON.stringify(msg.httpMetadata || {}),
|
|
2043
|
+
JSON.stringify(msg.customMetadata || {}),
|
|
2044
|
+
],
|
|
2045
|
+
);
|
|
2046
|
+
return { ok: true, key: msg.key, uploadId };
|
|
2047
|
+
}
|
|
2048
|
+
if (op === "r2.multipart.complete") {
|
|
2049
|
+
const upload = __sbSql(
|
|
2050
|
+
store,
|
|
2051
|
+
"SELECT http_json, custom_json FROM r2_upload WHERE bucket = ? AND key = ? AND upload_id = ?",
|
|
2052
|
+
[msg.bucket, msg.key, msg.uploadId],
|
|
2053
|
+
);
|
|
2054
|
+
if (!upload.rows.length) throw new Error("multipart upload not found");
|
|
2055
|
+
const stored = __sbSql(
|
|
2056
|
+
store,
|
|
2057
|
+
"SELECT part_number, size, etag FROM r2_part WHERE bucket = ? AND key = ? AND upload_id = ? ORDER BY part_number",
|
|
2058
|
+
[msg.bucket, msg.key, msg.uploadId],
|
|
2059
|
+
).rows;
|
|
2060
|
+
const requested = msg.parts || [];
|
|
2061
|
+
if (!requested.length) throw new Error("complete requires at least one uploaded part");
|
|
2062
|
+
if (requested.length !== stored.length) throw new Error("uploaded parts do not match");
|
|
2063
|
+
for (let i = 0; i < stored.length; i++) {
|
|
2064
|
+
const part = requested[i] || {};
|
|
2065
|
+
const partNumber = Number(part.partNumber);
|
|
2066
|
+
if (partNumber !== Number(stored[i][0]) || String(part.etag || "") !== String(stored[i][2]))
|
|
2067
|
+
throw new Error("uploaded parts do not match");
|
|
2068
|
+
if (i > 0 && partNumber <= Number(requested[i - 1].partNumber))
|
|
2069
|
+
throw new Error("uploaded parts must be ordered by partNumber");
|
|
2070
|
+
if (i < stored.length - 1 && Number(stored[i][1]) !== Number(stored[0][1]))
|
|
2071
|
+
throw new Error("all multipart parts except the last must have the same size");
|
|
2072
|
+
if (
|
|
2073
|
+
i === stored.length - 1 &&
|
|
2074
|
+
stored.length > 1 &&
|
|
2075
|
+
Number(stored[i][1]) > Number(stored[0][1])
|
|
2076
|
+
)
|
|
2077
|
+
throw new Error("the last multipart part cannot be larger than the others");
|
|
2078
|
+
}
|
|
2079
|
+
const previous = __sbSql(store, "SELECT blob_id FROM r2 WHERE bucket = ? AND key = ?", [
|
|
2080
|
+
msg.bucket,
|
|
2081
|
+
msg.key,
|
|
2082
|
+
]);
|
|
2083
|
+
const completed = JSON.parse(
|
|
2084
|
+
__sbR2MultipartCompleteRaw(store, String(msg.bucket), String(msg.key), String(msg.uploadId)),
|
|
2085
|
+
);
|
|
2086
|
+
if (completed.ok === false) throw new Error("r2 multipart complete: " + completed.error);
|
|
2087
|
+
const uploaded = new Date().toISOString();
|
|
2088
|
+
try {
|
|
2089
|
+
__sbSql(
|
|
2090
|
+
store,
|
|
2091
|
+
"INSERT INTO r2 (bucket, key, size, etag, uploaded, http_json, custom_json, blob_id) " +
|
|
2092
|
+
"VALUES (?1,?2,?3,?4,?5,?6,?7,?8) " +
|
|
2093
|
+
"ON CONFLICT (bucket, key) DO UPDATE SET size=?3, etag=?4, uploaded=?5, http_json=?6, custom_json=?7, blob_id=?8",
|
|
2094
|
+
[
|
|
2095
|
+
msg.bucket,
|
|
2096
|
+
msg.key,
|
|
2097
|
+
completed.size,
|
|
2098
|
+
completed.etag,
|
|
2099
|
+
uploaded,
|
|
2100
|
+
upload.rows[0][0],
|
|
2101
|
+
upload.rows[0][1],
|
|
2102
|
+
completed.etag,
|
|
2103
|
+
],
|
|
2104
|
+
);
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
if (!previous.rows.length || String(previous.rows[0][0] || "") !== String(completed.etag))
|
|
2107
|
+
__sbR2DeleteRaw(store, String(msg.bucket), String(msg.key), String(completed.etag));
|
|
2108
|
+
throw error;
|
|
2109
|
+
}
|
|
2110
|
+
if (previous.rows.length && String(previous.rows[0][0] || "") !== String(completed.etag))
|
|
2111
|
+
__sbR2DeleteRaw(
|
|
2112
|
+
store,
|
|
2113
|
+
String(msg.bucket),
|
|
2114
|
+
String(msg.key),
|
|
2115
|
+
String(previous.rows[0][0] || ""),
|
|
2116
|
+
);
|
|
2117
|
+
__sbR2MultipartDeleteRaw(store, String(msg.bucket), String(msg.key), String(msg.uploadId));
|
|
2118
|
+
__sbSql(store, "DELETE FROM r2_part WHERE bucket = ? AND key = ? AND upload_id = ?", [
|
|
2119
|
+
msg.bucket,
|
|
2120
|
+
msg.key,
|
|
2121
|
+
msg.uploadId,
|
|
2122
|
+
]);
|
|
2123
|
+
__sbSql(store, "DELETE FROM r2_upload WHERE bucket = ? AND key = ? AND upload_id = ?", [
|
|
2124
|
+
msg.bucket,
|
|
2125
|
+
msg.key,
|
|
2126
|
+
msg.uploadId,
|
|
2127
|
+
]);
|
|
2128
|
+
return {
|
|
2129
|
+
ok: true,
|
|
2130
|
+
object: {
|
|
2131
|
+
key: msg.key,
|
|
2132
|
+
size: completed.size,
|
|
2133
|
+
etag: completed.etag,
|
|
2134
|
+
uploaded,
|
|
2135
|
+
httpMetadata: JSON.parse(upload.rows[0][0] || "{}"),
|
|
2136
|
+
customMetadata: JSON.parse(upload.rows[0][1] || "{}"),
|
|
2137
|
+
},
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
if (op === "r2.multipart.abort") {
|
|
2141
|
+
const upload = __sbSql(
|
|
2142
|
+
store,
|
|
2143
|
+
"SELECT 1 FROM r2_upload WHERE bucket = ? AND key = ? AND upload_id = ?",
|
|
2144
|
+
[msg.bucket, msg.key, msg.uploadId],
|
|
2145
|
+
);
|
|
2146
|
+
if (!upload.rows.length) throw new Error("multipart upload not found");
|
|
2147
|
+
__sbR2MultipartDeleteRaw(store, String(msg.bucket), String(msg.key), String(msg.uploadId));
|
|
2148
|
+
__sbSql(store, "DELETE FROM r2_part WHERE bucket = ? AND key = ? AND upload_id = ?", [
|
|
2149
|
+
msg.bucket,
|
|
2150
|
+
msg.key,
|
|
2151
|
+
msg.uploadId,
|
|
2152
|
+
]);
|
|
2153
|
+
__sbSql(store, "DELETE FROM r2_upload WHERE bucket = ? AND key = ? AND upload_id = ?", [
|
|
2154
|
+
msg.bucket,
|
|
2155
|
+
msg.key,
|
|
2156
|
+
msg.uploadId,
|
|
2157
|
+
]);
|
|
2158
|
+
return { ok: true };
|
|
1463
2159
|
}
|
|
1464
2160
|
if (op === "r2.get" || op === "r2.head") {
|
|
1465
2161
|
// head must not read the blob file: reading an 8 MB object only to drop
|
|
@@ -1467,7 +2163,7 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1467
2163
|
const wantsBody = op === "r2.get";
|
|
1468
2164
|
const r = __sbSql(
|
|
1469
2165
|
store,
|
|
1470
|
-
"SELECT size, etag, uploaded, http_json, custom_json FROM r2 WHERE bucket = ? AND key = ?",
|
|
2166
|
+
"SELECT size, etag, uploaded, http_json, custom_json, blob_id FROM r2 WHERE bucket = ? AND key = ?",
|
|
1471
2167
|
[msg.bucket, msg.key],
|
|
1472
2168
|
);
|
|
1473
2169
|
if (!r.rows.length) return { ok: true, found: false };
|
|
@@ -1484,12 +2180,25 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1484
2180
|
httpMetadata: JSON.parse(row[3] || "{}"),
|
|
1485
2181
|
customMetadata: JSON.parse(row[4] || "{}"),
|
|
1486
2182
|
},
|
|
1487
|
-
|
|
2183
|
+
blobId: row[5] || "",
|
|
2184
|
+
body: wantsBody
|
|
2185
|
+
? __sbR2GetRaw(store, String(msg.bucket), String(msg.key), String(row[5] || ""))
|
|
2186
|
+
: undefined,
|
|
1488
2187
|
};
|
|
1489
2188
|
}
|
|
1490
2189
|
if (op === "r2.delete") {
|
|
2190
|
+
const existing = __sbSql(store, "SELECT blob_id FROM r2 WHERE bucket = ? AND key = ?", [
|
|
2191
|
+
msg.bucket,
|
|
2192
|
+
msg.key,
|
|
2193
|
+
]);
|
|
1491
2194
|
__sbSql(store, "DELETE FROM r2 WHERE bucket = ? AND key = ?", [msg.bucket, msg.key]);
|
|
1492
|
-
|
|
2195
|
+
if (existing.rows.length)
|
|
2196
|
+
__sbR2DeleteRaw(
|
|
2197
|
+
store,
|
|
2198
|
+
String(msg.bucket),
|
|
2199
|
+
String(msg.key),
|
|
2200
|
+
String(existing.rows[0][0] || ""),
|
|
2201
|
+
);
|
|
1493
2202
|
return { ok: true };
|
|
1494
2203
|
}
|
|
1495
2204
|
if (op === "r2.list") {
|
|
@@ -1515,12 +2224,11 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1515
2224
|
if (op === "queue.send" || op === "queue.send_batch") {
|
|
1516
2225
|
const items = op === "queue.send" ? [msg.body] : msg.messages || [];
|
|
1517
2226
|
for (let i = 0; i < items.length; i++) {
|
|
1518
|
-
__sbSql(
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
String(items[i]),
|
|
1522
|
-
|
|
1523
|
-
]);
|
|
2227
|
+
__sbSql(
|
|
2228
|
+
store,
|
|
2229
|
+
"INSERT INTO mq (queue, id, body, visible_at, attempts, dead) VALUES (?1,?2,?3,?4,0,0)",
|
|
2230
|
+
[msg.queue, __sbHex(12), String(items[i]), Date.now()],
|
|
2231
|
+
);
|
|
1524
2232
|
}
|
|
1525
2233
|
return { ok: true };
|
|
1526
2234
|
}
|
|
@@ -1531,7 +2239,9 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1531
2239
|
msg.id,
|
|
1532
2240
|
msg.key,
|
|
1533
2241
|
]);
|
|
1534
|
-
return r.rows.length
|
|
2242
|
+
return r.rows.length
|
|
2243
|
+
? { ok: true, found: true, value: r.rows[0][0] }
|
|
2244
|
+
: { ok: true, found: false };
|
|
1535
2245
|
}
|
|
1536
2246
|
if (op === "do.storage.put") {
|
|
1537
2247
|
__sbSql(
|
|
@@ -1542,7 +2252,11 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1542
2252
|
return { ok: true };
|
|
1543
2253
|
}
|
|
1544
2254
|
if (op === "do.storage.delete") {
|
|
1545
|
-
const r = __sbSql(store, "DELETE FROM do_storage WHERE cls = ? AND id = ? AND key = ?", [
|
|
2255
|
+
const r = __sbSql(store, "DELETE FROM do_storage WHERE cls = ? AND id = ? AND key = ?", [
|
|
2256
|
+
msg.cls,
|
|
2257
|
+
msg.id,
|
|
2258
|
+
msg.key,
|
|
2259
|
+
]);
|
|
1546
2260
|
return { ok: true, deleted: r.changes > 0 };
|
|
1547
2261
|
}
|
|
1548
2262
|
if (op === "do.storage.delete_all") {
|
|
@@ -1579,13 +2293,17 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1579
2293
|
}
|
|
1580
2294
|
|
|
1581
2295
|
if (op === "ae.write") {
|
|
1582
|
-
__sbSql(
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
2296
|
+
__sbSql(
|
|
2297
|
+
store,
|
|
2298
|
+
"INSERT INTO ae (dataset, ts, indexes_json, blobs_json, doubles_json) VALUES (?1,?2,?3,?4,?5)",
|
|
2299
|
+
[
|
|
2300
|
+
msg.dataset,
|
|
2301
|
+
Date.now(),
|
|
2302
|
+
JSON.stringify(msg.indexes || []),
|
|
2303
|
+
JSON.stringify(msg.blobs || []),
|
|
2304
|
+
JSON.stringify(msg.doubles || []),
|
|
2305
|
+
],
|
|
2306
|
+
);
|
|
1589
2307
|
return { ok: true };
|
|
1590
2308
|
}
|
|
1591
2309
|
if (op === "ae.query") {
|
|
@@ -1664,14 +2382,50 @@ globalThis.__sbR2Put = function (bucket, key, body, httpMetadata, customMetadata
|
|
|
1664
2382
|
),
|
|
1665
2383
|
);
|
|
1666
2384
|
if (reply.ok === false) throw new Error("sproutboat r2.put: " + reply.error);
|
|
1667
|
-
return {
|
|
2385
|
+
return {
|
|
2386
|
+
object: { key: String(key), size: reply.size, etag: reply.etag, uploaded: reply.uploaded },
|
|
2387
|
+
};
|
|
1668
2388
|
};
|
|
1669
2389
|
|
|
1670
2390
|
globalThis.__sbR2Get = function (bucket, key) {
|
|
1671
2391
|
// Metadata through the normal path (small), bytes through their own.
|
|
1672
2392
|
const meta = __sbEmbeddedDispatch({ op: "r2.head", bucket, key });
|
|
1673
2393
|
if (!meta.found) return { found: false };
|
|
1674
|
-
return {
|
|
2394
|
+
return {
|
|
2395
|
+
found: true,
|
|
2396
|
+
object: meta.object,
|
|
2397
|
+
body: __sbR2GetRaw(__sbStore(), String(bucket), String(key), String(meta.blobId || "")),
|
|
2398
|
+
};
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
globalThis.__sbR2MultipartPut = function (bucket, key, uploadId, partNumber, body) {
|
|
2402
|
+
__sbEnsureSchema();
|
|
2403
|
+
const store = __sbStore();
|
|
2404
|
+
const upload = __sbSql(
|
|
2405
|
+
store,
|
|
2406
|
+
"SELECT 1 FROM r2_upload WHERE bucket = ? AND key = ? AND upload_id = ?",
|
|
2407
|
+
[bucket, key, uploadId],
|
|
2408
|
+
);
|
|
2409
|
+
if (!upload.rows.length)
|
|
2410
|
+
throw new Error("sproutboat r2.multipart.put: multipart upload not found");
|
|
2411
|
+
const reply = JSON.parse(
|
|
2412
|
+
__sbR2MultipartPutRaw(
|
|
2413
|
+
store,
|
|
2414
|
+
String(bucket),
|
|
2415
|
+
String(key),
|
|
2416
|
+
String(uploadId),
|
|
2417
|
+
String(partNumber),
|
|
2418
|
+
body,
|
|
2419
|
+
),
|
|
2420
|
+
);
|
|
2421
|
+
if (reply.ok === false) throw new Error("sproutboat r2.multipart.put: " + reply.error);
|
|
2422
|
+
__sbSql(
|
|
2423
|
+
store,
|
|
2424
|
+
"INSERT INTO r2_part (bucket, key, upload_id, part_number, size, etag) VALUES (?1,?2,?3,?4,?5,?6) " +
|
|
2425
|
+
"ON CONFLICT (bucket, key, upload_id, part_number) DO UPDATE SET size=?5, etag=?6",
|
|
2426
|
+
[bucket, key, uploadId, partNumber, reply.size, reply.etag],
|
|
2427
|
+
);
|
|
2428
|
+
return { ok: true, part: { partNumber, etag: reply.etag } };
|
|
1675
2429
|
};
|
|
1676
2430
|
|
|
1677
2431
|
globalThis.__sbAssetsGet = function (path) {
|
|
@@ -1779,7 +2533,10 @@ globalThis.__sbStartLocalTriggers = function (handlers, bindings) {
|
|
|
1779
2533
|
if (p && __sbIsFn(p.then)) __sbTasks.push(p);
|
|
1780
2534
|
},
|
|
1781
2535
|
};
|
|
1782
|
-
const res = handlers.scheduled(
|
|
2536
|
+
const res = handlers.scheduled(
|
|
2537
|
+
{ cron: crons[i], scheduledTime: now.getTime(), noRetry() {} },
|
|
2538
|
+
ctx,
|
|
2539
|
+
);
|
|
1783
2540
|
if (res && __sbIsFn(res.then)) await res;
|
|
1784
2541
|
if (__sbTasks.length) await __sbDrainWaitUntil(__sbTasks);
|
|
1785
2542
|
}
|
|
@@ -1837,9 +2594,11 @@ globalThis.__sbStartLocalTriggers = function (handlers, bindings) {
|
|
|
1837
2594
|
setInterval(async function () {
|
|
1838
2595
|
__sbEnsureSchema();
|
|
1839
2596
|
const now = Date.now();
|
|
1840
|
-
const due = __sbSql(
|
|
1841
|
-
|
|
1842
|
-
|
|
2597
|
+
const due = __sbSql(
|
|
2598
|
+
store,
|
|
2599
|
+
"SELECT cls, id, at, attempts FROM do_alarm WHERE at <= ? ORDER BY at LIMIT 10",
|
|
2600
|
+
[now],
|
|
2601
|
+
);
|
|
1843
2602
|
for (let i = 0; i < due.rows.length; i++) {
|
|
1844
2603
|
const cls = due.rows[i][0];
|
|
1845
2604
|
const id = due.rows[i][1];
|