sqlite_web_vfs 1.0.1

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.
@@ -0,0 +1,823 @@
1
+ #pragma once
2
+
3
+ #include "HTTP.h"
4
+ #include "SQLiteVFS.h"
5
+ #include "ThreadPool.h"
6
+ #include <future>
7
+ #include <set>
8
+ #include <sstream>
9
+ #include <sys/stat.h>
10
+ #include <sys/types.h>
11
+ #include <unistd.h>
12
+
13
+ namespace WebVFS {
14
+
15
+ #include "dbi.h"
16
+
17
+ class Timer {
18
+ unsigned long long t0_;
19
+
20
+ public:
21
+ Timer() {
22
+ timeval t0;
23
+ gettimeofday(&t0, nullptr);
24
+ t0_ = t0.tv_sec * 1000000ULL + t0.tv_usec;
25
+ }
26
+
27
+ unsigned long long micros() {
28
+ timeval tv;
29
+ gettimeofday(&tv, nullptr);
30
+ return tv.tv_sec * 1000000ULL + tv.tv_usec - t0_;
31
+ }
32
+ };
33
+
34
+ // Extent: a contiguous portion of the remote file which we keep cached in memory. Three size
35
+ // options to balance latency, throughput, read amplification, HTTP request count.
36
+ struct Extent {
37
+ enum Size { SM, MD, LG };
38
+
39
+ size_t small_KiB;
40
+ Size size;
41
+ size_t rank;
42
+
43
+ static size_t Bytes(Size sz, size_t small_KiB) {
44
+ switch (sz) {
45
+ case Size::SM:
46
+ return small_KiB << 10;
47
+ case Size::MD:
48
+ return small_KiB << 14;
49
+ default:
50
+ break;
51
+ }
52
+ return small_KiB << 18;
53
+ }
54
+
55
+ Extent(Size size_, size_t rank_, size_t small_KiB_)
56
+ : small_KiB(small_KiB_), size(size_), rank(rank_) {}
57
+ Extent() : small_KiB(64), size(Size::SM), rank(0) {} // don't use; for STL
58
+
59
+ size_t Bytes(Size sz) const { return Bytes(sz, small_KiB); }
60
+
61
+ bool operator<(const Extent &rhs) const {
62
+ assert(rhs.small_KiB == small_KiB);
63
+ return size < rhs.size || (size == rhs.size && rank < rhs.rank);
64
+ }
65
+
66
+ Extent Prev() const {
67
+ if (rank == 0) {
68
+ throw std::runtime_error("illegal Extent::Prev()");
69
+ }
70
+ return Extent(size, rank - 1, small_KiB);
71
+ }
72
+
73
+ Extent Next() const { return Extent(size, rank + 1, small_KiB); }
74
+
75
+ Extent Promote() const {
76
+ if (size == Size::SM) {
77
+ return Extent(Size::MD, rank / 16, small_KiB);
78
+ }
79
+ if (size == Size::MD) {
80
+ return Extent(Size::LG, rank / 16, small_KiB);
81
+ }
82
+ return *this;
83
+ }
84
+
85
+ uint64_t Offset() const { return Bytes(size) * rank; }
86
+
87
+ size_t Bytes() const { return Bytes(size); }
88
+
89
+ bool Contains(uint64_t offset, size_t length) const {
90
+ return offset >= Offset() && offset + length <= Offset() + Bytes();
91
+ }
92
+
93
+ bool Contains(const Extent &rhs) const { return Contains(rhs.Offset(), rhs.Bytes()); }
94
+
95
+ bool Exists(uint64_t file_size_) const { return Offset() < file_size_; }
96
+
97
+ std::string str(uint64_t file_size) const {
98
+ auto hi = std::min((Offset() + Bytes() - 1), file_size - 1);
99
+ std::ostringstream fmt_range;
100
+ fmt_range << "bytes=" << Offset() << "-" << hi;
101
+ return fmt_range.str();
102
+ }
103
+ };
104
+
105
+ class File : public SQLiteVFS::File {
106
+ const std::string uri_, filename_;
107
+ const sqlite_int64 file_size_;
108
+ const size_t small_KiB_;
109
+ const std::unique_ptr<HTTP::CURLpool> curlpool_;
110
+ std::unique_ptr<dbiHelper> dbi_;
111
+
112
+ // Extents cached for potential reuse, with a last-use timestamp for LRU eviction.
113
+ // Note: The purpose of keeping extents cached is to anticipate future Read requests for nearby
114
+ // database pages. It is NOT to serve repeat Reads for the same page, which is the job of
115
+ // the SQLite page cache in front of the VFS.
116
+ using shared_string = std::shared_ptr<const std::string>;
117
+ struct ResidentExtent {
118
+ const Extent extent;
119
+ shared_string data;
120
+ uint64_t seqno;
121
+ bool used = false; // true after a prefetched extent is actually used
122
+ ResidentExtent(Extent extent_, const shared_string &data_, uint64_t seqno_)
123
+ : extent(extent_), data(data_), seqno(seqno_) {}
124
+ };
125
+ std::map<Extent, ResidentExtent> resident_;
126
+ std::map<uint64_t, Extent> usage_; // secondary index of resident_ ordered by seqno (for LRU)
127
+ uint64_t extent_seqno_ = 0; // timestamp
128
+ ThreadPoolWithEnqueueFast threadpool_;
129
+
130
+ // The main thread locks mu_ to update resident_ and usage_, but may read them without.
131
+ // Background threads may (only) read them with mu_ locked.
132
+ // The other state below always requires mu_ to read or write.
133
+ std::mutex mu_;
134
+
135
+ #define SQLITE_WEB_LOG(msg_level, msg) \
136
+ if (log_.level() >= msg_level) { \
137
+ std::unique_lock<std::mutex> log_lock(mu_); \
138
+ SQLITE_VFS_LOG(msg_level, '[' << filename_ << "] " << msg) \
139
+ }
140
+
141
+ // state for [pre]fetch on background threads
142
+ std::deque<Extent> fetch_queue_; // queue of fetch ops
143
+ std::map<Extent, size_t> fetch_queue2_; // secondary index of fetch_queue_
144
+ std::set<Extent> fetch_wip_; // fetch ops currently underway
145
+ std::map<Extent, shared_string> fetch_done_; // completed fetches waiting to be merged
146
+ std::map<Extent, int> fetch_error_; // error codes
147
+ std::condition_variable fetch_cv_; // on add to fetch_done_ or fetch_error_
148
+
149
+ // performance counters
150
+ uint64_t read_count_ = 0, dbi_read_count_ = 0, fetch_count_ = 0, ideal_prefetch_count_ = 0,
151
+ wasted_prefetch_count_ = 0, read_bytes_ = 0, fetch_bytes_ = 0, stalled_micros_ = 0;
152
+
153
+ // Run HTTP GET request for an extent
154
+ // mu_ MUST NOT be locked for this (otherwise it'll hang, due to logging)
155
+ int FetchExtent(Extent extent, shared_string &data) {
156
+ Timer t;
157
+ const std::string protocol = uri_.substr(0, 6) == "https:" ? "HTTPS" : "HTTP";
158
+ try {
159
+ HTTP::headers reqhdrs, reshdrs;
160
+ reqhdrs["range"] = extent.str(file_size_);
161
+ SQLITE_WEB_LOG(5, protocol << " GET " << reqhdrs["range"] << " ...")
162
+
163
+ long status = -1;
164
+ bool retried = false;
165
+ std::shared_ptr<std::string> body(new std::string());
166
+ HTTP::RetryOptions options;
167
+ options.min_response_body =
168
+ std::min(uint64_t(extent.Bytes()), uint64_t(file_size_ - extent.Offset()));
169
+ options.connpool = curlpool_.get();
170
+ options.on_retry = [&](HTTP::Method method, const std::string &url,
171
+ const HTTP::headers &request_headers, CURLcode rc,
172
+ long response_code, const HTTP::headers &response_headers,
173
+ const std::string &response_body, unsigned int attempt) {
174
+ retried = true;
175
+ if (log_.level() >= 3) {
176
+ std::string msg = curl_easy_strerror(rc);
177
+ if (rc == CURLE_OK) {
178
+ if (response_code < 200 || response_code >= 300) {
179
+ msg = "status = " + std::to_string(response_code);
180
+ } else {
181
+ msg = "unexpected response body size " +
182
+ std::to_string(response_body.size()) +
183
+ " != " + std::to_string(options.min_response_body);
184
+ }
185
+ }
186
+ SQLITE_WEB_LOG(3, protocol << " GET " << reqhdrs["range"] << " retrying " << msg
187
+ << " (attempt " << attempt << " of "
188
+ << options.max_tries << "; " << (t.micros() / 1000)
189
+ << "ms elapsed)")
190
+ }
191
+ };
192
+ auto rc = HTTP::RetryGet(uri_, reqhdrs, status, reshdrs, *body, options);
193
+ if (rc != CURLE_OK) {
194
+ SQLITE_WEB_LOG(1, protocol << " GET " << reqhdrs["range"] << ' '
195
+ << curl_easy_strerror(rc) << " (" << (t.micros() / 1000)
196
+ << "ms)")
197
+ return SQLITE_IOERR_READ;
198
+ }
199
+ if (status < 200 || status >= 300) {
200
+ SQLITE_WEB_LOG(1, protocol << " GET " << reqhdrs["range"] << " error status = "
201
+ << status << " (" << (t.micros() / 1000) << "ms)")
202
+ return SQLITE_IOERR_READ;
203
+ }
204
+ if (body->size() != options.min_response_body) {
205
+ SQLITE_WEB_LOG(1, protocol << " GET " << reqhdrs["range"]
206
+ << " incorrect response body length = " << body->size()
207
+ << ", expected = " << extent.Bytes())
208
+ return SQLITE_IOERR_SHORT_READ;
209
+ }
210
+ data = body;
211
+ if (log_.level() >= 4) {
212
+ SQLITE_WEB_LOG(4, protocol << " GET " << reqhdrs["range"] << " OK ("
213
+ << data->size() / 1024 << "KiB, " << (t.micros() / 1000)
214
+ << "ms)")
215
+ } else if (log_.level() >= 2 && retried) {
216
+ SQLITE_WEB_LOG(4, protocol << " GET " << reqhdrs["range"] << " OK after retry ("
217
+ << data->size() / 1024 << "KiB, " << (t.micros() / 1000)
218
+ << "ms)")
219
+ }
220
+ return SQLITE_OK;
221
+ } catch (std::bad_alloc &) {
222
+ return SQLITE_IOERR_NOMEM;
223
+ } catch (std::exception &exn) {
224
+ SQLITE_WEB_LOG(1, protocol << " GET: " << exn.what());
225
+ return SQLITE_IOERR_READ;
226
+ }
227
+ }
228
+
229
+ static void *FetchJob(void *v) {
230
+ File *self = (File *)v;
231
+
232
+ std::unique_lock<std::mutex> lock(self->mu_);
233
+
234
+ // dequeue a desired extent
235
+ if (self->fetch_queue_.empty()) {
236
+ return nullptr;
237
+ }
238
+ Extent extent = self->fetch_queue_.front();
239
+ self->fetch_queue_.pop_front();
240
+ auto fq2c = self->fetch_queue2_.find(extent);
241
+ assert(fq2c != self->fetch_queue2_.end() && fq2c->second);
242
+ fq2c->second -= 1;
243
+ if (!fq2c->second) {
244
+ self->fetch_queue2_.erase(extent);
245
+ }
246
+
247
+ // coalesce request if same extent or one containing it is already wip or done
248
+ Extent container = extent;
249
+ for (int i = 0; i < 3; ++i) {
250
+ if (self->resident_.find(container) != self->resident_.end() ||
251
+ self->fetch_wip_.find(container) != self->fetch_wip_.end() ||
252
+ self->fetch_done_.find(container) != self->fetch_done_.end()) {
253
+ return nullptr;
254
+ }
255
+ container = container.Promote();
256
+ }
257
+
258
+ // run HTTP GET
259
+ self->fetch_wip_.insert(extent);
260
+ lock.unlock();
261
+ shared_string buf;
262
+ int rc = self->FetchExtent(extent, buf);
263
+ lock.lock();
264
+
265
+ // record result
266
+ self->fetch_wip_.erase(extent);
267
+ assert(self->fetch_done_.find(extent) == self->fetch_done_.end() &&
268
+ self->fetch_error_.find(extent) == self->fetch_error_.end());
269
+ if (rc == SQLITE_OK) {
270
+ self->fetch_count_++;
271
+ self->fetch_bytes_ += buf->size();
272
+ self->fetch_done_.emplace(extent, buf);
273
+ } else {
274
+ self->fetch_error_[extent] = rc;
275
+ }
276
+ lock.unlock();
277
+ self->fetch_cv_.notify_all();
278
+
279
+ return nullptr;
280
+ }
281
+
282
+ void EnqueueFetch(std::unique_lock<std::mutex> &lock, Extent extent, bool front = false) {
283
+ assert(lock.owns_lock());
284
+ auto fq2c = fetch_queue2_.find(extent);
285
+ if (front) {
286
+ fetch_queue_.push_front(extent);
287
+ } else if (fq2c == fetch_queue2_.end()) {
288
+ fetch_queue_.push_back(extent);
289
+ } else {
290
+ return;
291
+ }
292
+ if (fq2c == fetch_queue2_.end()) {
293
+ fetch_queue2_[extent] = 1;
294
+ } else {
295
+ fq2c->second += 1;
296
+ }
297
+ threadpool_.EnqueueFast(this, FetchJob, nullptr);
298
+ }
299
+
300
+ void UpdateResident(std::unique_lock<std::mutex> &lock) {
301
+ // main thread collects results of recent background fetch jobs
302
+ assert(lock.owns_lock());
303
+
304
+ // surface any errors recorded
305
+ auto err = fetch_error_.begin();
306
+ if (err != fetch_error_.end()) {
307
+ int rc = err->second;
308
+ fetch_error_.erase(err);
309
+ throw rc;
310
+ }
311
+
312
+ // merge successfully fetched extents into resident_ (& update usage_)
313
+ for (auto p = fetch_done_.begin(); p != fetch_done_.end(); p = fetch_done_.erase(p)) {
314
+ Extent extent = p->first;
315
+ if (resident_.find(extent) == resident_.end()) {
316
+ usage_[++extent_seqno_] = extent;
317
+ resident_.emplace(extent, ResidentExtent(extent, p->second, extent_seqno_));
318
+ }
319
+ }
320
+ }
321
+
322
+ Extent ExtentContaining(uint64_t offset, size_t length) {
323
+ auto rk = offset / Extent::Bytes(Extent::Size::SM, small_KiB_);
324
+ auto hi = offset + std::max(length, size_t(1)) - 1;
325
+ if (rk == hi / Extent::Bytes(Extent::Size::SM, small_KiB_)) {
326
+ return Extent(Extent::Size::SM, rk, small_KiB_);
327
+ }
328
+ throw std::runtime_error("unaligned Read");
329
+ }
330
+
331
+ std::map<Extent, ResidentExtent>::iterator FindResidentExtent(uint64_t offset, size_t length) {
332
+ Extent extent = ExtentContaining(offset, length), extent2 = extent.Promote(),
333
+ extent3 = extent2.Promote();
334
+ // prefer largest
335
+ auto line = resident_.find(extent3);
336
+ if (line != resident_.end())
337
+ return line;
338
+ line = resident_.find(extent2);
339
+ if (line != resident_.end())
340
+ return line;
341
+ line = resident_.find(extent);
342
+ return line;
343
+ }
344
+
345
+ bool ResidentAndUsed(Extent extent) {
346
+ auto p = resident_.find(extent);
347
+ return p != resident_.end() && p->second.used;
348
+ }
349
+
350
+ void EvictResident(std::unique_lock<std::mutex> &lock, size_t n) {
351
+ assert(lock.owns_lock());
352
+ while (resident_.size() > n) {
353
+ auto lru = usage_.begin();
354
+ auto lru_line = resident_.find(lru->second);
355
+ assert(lru_line != resident_.end());
356
+ if (!lru_line->second.used) {
357
+ wasted_prefetch_count_++;
358
+ }
359
+ resident_.erase(lru_line);
360
+ usage_.erase(lru->first);
361
+ }
362
+ }
363
+
364
+ ResidentExtent EnsureResidentExtent(uint64_t offset, size_t length) {
365
+ auto last_used = usage_.rbegin();
366
+ if (last_used != usage_.rend() && last_used->second.Contains(offset, length)) {
367
+ // most-recently used extent still has the page we need (hot path, doesn't lock mu_)
368
+ auto p = resident_.find(last_used->second);
369
+ assert(p != resident_.end());
370
+ return p->second;
371
+ }
372
+
373
+ std::unique_lock<std::mutex> lock(mu_);
374
+ // find a resident extent containing desired page
375
+ UpdateResident(lock);
376
+ auto line = FindResidentExtent(offset, length);
377
+ bool blocked = false;
378
+
379
+ if (line == resident_.end()) {
380
+ // needed extent not resident: front-enqueue job to fetch it
381
+ Extent extent = ExtentContaining(offset, length);
382
+ // promote up to medium if we already used the previous small or medium extent
383
+ Extent container = extent.Promote();
384
+ if ((extent.rank > 0 && ResidentAndUsed(extent.Prev())) ||
385
+ ResidentAndUsed(extent.Next()) ||
386
+ (container.rank > 0 && ResidentAndUsed(container.Prev())) ||
387
+ ResidentAndUsed(container.Next())) {
388
+ extent = container;
389
+ }
390
+ EnqueueFetch(lock, extent, true);
391
+ // wait for it
392
+ do {
393
+ fetch_cv_.wait(lock);
394
+ UpdateResident(lock);
395
+ } while ((line = FindResidentExtent(offset, length)) == resident_.end());
396
+ blocked = true;
397
+ }
398
+
399
+ Extent extent = line->first;
400
+ ResidentExtent &res = line->second;
401
+ assert(!(extent < res.extent || res.extent < extent));
402
+ // LRU bookkeeping: delete old seqno_ from the secondary index, then record the new one
403
+ assert(usage_.find(res.seqno) != usage_.end());
404
+ usage_.erase(res.seqno); // old seqno
405
+ usage_[++extent_seqno_] = extent;
406
+ res.seqno = extent_seqno_;
407
+ if (!blocked && !res.used) {
408
+ ideal_prefetch_count_++;
409
+ }
410
+ res.used = true;
411
+ // now evict LRU extents if needed
412
+ EvictResident(lock, 32);
413
+
414
+ // if appropriate, initiate prefetch of nearby/surrounding extents
415
+ if (extent.size < Extent::Size::LG) {
416
+ // if we used the previous or next extent of the same size, initiate prefetch of the
417
+ // next size up
418
+ if ((extent.rank > 0 && ResidentAndUsed(extent.Prev())) ||
419
+ (ResidentAndUsed(extent.Next()))) {
420
+ EnqueueFetch(lock, extent.Promote());
421
+ }
422
+ } else {
423
+ // large size: prefetch up to 4 subsequent/preceding extents if we seem to have
424
+ // momentum in a contiguous scan
425
+ Extent pred = extent, succ = extent;
426
+ for (int ofs = 0; ofs < 4 && pred.rank; ++ofs) {
427
+ pred = pred.Prev();
428
+ succ = succ.Next();
429
+ if (ResidentAndUsed(pred) && succ.Exists(file_size_)) {
430
+ EnqueueFetch(lock, succ);
431
+ } else {
432
+ break;
433
+ }
434
+ }
435
+ pred = extent;
436
+ succ = extent;
437
+ for (int ofs = 0; ofs < 4 && pred.rank; ++ofs) {
438
+ pred = pred.Prev();
439
+ succ = succ.Next();
440
+ if (ResidentAndUsed(succ)) {
441
+ EnqueueFetch(lock, pred);
442
+ } else {
443
+ break;
444
+ }
445
+ }
446
+ }
447
+
448
+ return res;
449
+ }
450
+
451
+ int Read(void *zBuf, int iAmt, sqlite3_int64 iOfst) override {
452
+ Timer t;
453
+ if (iAmt < 0 || iOfst < 0) {
454
+ return SQLITE_IOERR_READ;
455
+ }
456
+ try {
457
+ int dbi_rc = SQLITE_NOTFOUND;
458
+ if (dbi_) {
459
+ // shortcut: serve page from .dbi if available
460
+ dbi_rc = dbi_->Seek(iOfst);
461
+ if (dbi_rc == SQLITE_OK) {
462
+ if (dbi_->PageSize() >= iAmt) {
463
+ memcpy(zBuf, dbi_->PageData(), iAmt);
464
+ dbi_read_count_++;
465
+ } else {
466
+ SQLITE_WEB_LOG(1, "unexpected page size " << dbi_->PageSize()
467
+ << " in .dbi @ offset " << iOfst)
468
+ }
469
+ } else if (dbi_rc != SQLITE_NOTFOUND) {
470
+ SQLITE_WEB_LOG(1, " failed reading page @ offset "
471
+ << iOfst << " from .dbi: " << dbi_->GetLastError())
472
+ }
473
+ }
474
+ if (dbi_rc != SQLITE_OK) {
475
+ // main path
476
+ ResidentExtent resext = EnsureResidentExtent(iOfst, iAmt);
477
+ assert(resext.extent.Contains(iOfst, iAmt));
478
+ assert(resext.data->size() + resext.extent.Offset() >= iOfst + iAmt);
479
+ memcpy(zBuf, resext.data->c_str() + (iOfst - resext.extent.Offset()), iAmt);
480
+ }
481
+ read_count_++;
482
+ read_bytes_ += iAmt;
483
+ stalled_micros_ += t.micros();
484
+ return SQLITE_OK;
485
+ } catch (int rc) {
486
+ return SQLITE_IOERR_READ;
487
+ } catch (std::bad_alloc &) {
488
+ return SQLITE_IOERR_NOMEM;
489
+ }
490
+ }
491
+
492
+ int Close() override {
493
+ {
494
+ std::unique_lock<std::mutex> lock(mu_);
495
+ fetch_queue_.clear();
496
+ fetch_queue2_.clear();
497
+ // TODO: abort ongoing requests via atomic<bool> passed into libcurl progress function
498
+ lock.unlock();
499
+ threadpool_.Barrier();
500
+ lock.lock();
501
+ UpdateResident(lock);
502
+ EvictResident(lock, 0); // ensure we count wasted prefetches
503
+ }
504
+ if (log_.level() >= 4) {
505
+ std::ostringstream msg;
506
+ msg << "page reads: " << read_count_;
507
+ if (dbi_) {
508
+ msg << " (from .dbi: " << dbi_read_count_ << ")";
509
+ }
510
+ SQLITE_WEB_LOG(
511
+ 4, msg.str() << ", HTTP GETs: " << fetch_count_ << " (prefetches ideal: "
512
+ << ideal_prefetch_count_ << ", wasted: " << wasted_prefetch_count_
513
+ << "), bytes read / downloaded / filesize: " << read_bytes_ << " / "
514
+ << fetch_bytes_ << " / " << file_size_ << ", stalled for "
515
+ << (stalled_micros_ / 1000)
516
+ << "ms; total connections: " << curlpool_->cumulative_connections())
517
+ }
518
+ // deletes this:
519
+ return SQLiteVFS::File::Close();
520
+ }
521
+
522
+ int Write(const void *zBuf, int iAmt, sqlite3_int64 iOfst) override { return SQLITE_MISUSE; }
523
+ int Truncate(sqlite3_int64 size) override { return SQLITE_MISUSE; }
524
+ int Sync(int flags) override { return SQLITE_MISUSE; }
525
+ int FileSize(sqlite3_int64 *pSize) override {
526
+ *pSize = (sqlite3_int64)file_size_;
527
+ return SQLITE_OK;
528
+ }
529
+ int Lock(int eLock) override { return SQLITE_OK; }
530
+ int Unlock(int eLock) override { return SQLITE_OK; }
531
+ int CheckReservedLock(int *pResOut) override {
532
+ *pResOut = 0;
533
+ return SQLITE_OK;
534
+ }
535
+ int FileControl(int op, void *pArg) override { return SQLITE_NOTFOUND; }
536
+ int SectorSize() override { return 0; }
537
+ int DeviceCharacteristics() override { return SQLITE_IOCAP_IMMUTABLE; }
538
+
539
+ int ShmMap(int iPg, int pgsz, int isWrite, void volatile **pp) override {
540
+ return SQLITE_MISUSE;
541
+ }
542
+ int ShmLock(int offset, int n, int flags) override { return SQLITE_MISUSE; }
543
+ void ShmBarrier() override {}
544
+ int ShmUnmap(int deleteFlag) override { return SQLITE_MISUSE; }
545
+
546
+ int Fetch(sqlite3_int64 iOfst, int iAmt, void **pp) override { return SQLITE_MISUSE; }
547
+ int Unfetch(sqlite3_int64 iOfst, void *p) override { return SQLITE_MISUSE; }
548
+
549
+ public:
550
+ File(const char *zName, const std::string &uri, const std::string &filename,
551
+ sqlite_int64 file_size, std::unique_ptr<HTTP::CURLpool> &&curlpool,
552
+ std::unique_ptr<dbiHelper> &&dbi, size_t small_KiB)
553
+ : SQLiteVFS::File(zName), uri_(uri), filename_(filename), file_size_(file_size),
554
+ curlpool_(std::move(curlpool)), dbi_(std::move(dbi)), threadpool_(4, 16),
555
+ small_KiB_(small_KiB) {
556
+ methods_.iVersion = 1;
557
+ log_.DetectLevel(zName, 0);
558
+ }
559
+ };
560
+
561
+ class VFS : public SQLiteVFS::Wrapper {
562
+ protected:
563
+ std::string last_error_;
564
+
565
+ int Open(const char *zName, sqlite3_file *pFile, int flags, int *pOutFlags) override {
566
+ if (!zName || strcmp(zName, "/__web__")) {
567
+ return wrapped_->xOpen(wrapped_, zName, pFile, flags, pOutFlags);
568
+ }
569
+
570
+ const char *encoded_uri = sqlite3_uri_parameter(zName, "web_url");
571
+ if (!encoded_uri || !encoded_uri[0]) {
572
+ last_error_ = "set web_url query parameter to percent-encoded URI";
573
+ return SQLITE_CANTOPEN;
574
+ }
575
+ if (!(flags & SQLITE_OPEN_READONLY)) {
576
+ last_error_ = "web access is read-only";
577
+ return SQLITE_CANTOPEN;
578
+ }
579
+ SQLiteVFS::Logger log_(zName, 0); // intentionally shadow this->log_
580
+ bool insecure = sqlite3_uri_int64(zName, "web_insecure", 0) == 1;
581
+ const char *env_insecure = getenv("SQLITE_WEB_INSECURE");
582
+ if (env_insecure && *env_insecure) {
583
+ errno = 0;
584
+ unsigned long env_insecure_i = strtoul(env_insecure, nullptr, 10);
585
+ if (errno == 0 && env_insecure_i == 1) {
586
+ insecure = true;
587
+ }
588
+ }
589
+ auto small_KiB = sqlite3_uri_int64(zName, "web_small_KiB", 64);
590
+ const char *env_small_KiB = getenv("SQLITE_WEB_SMALL_KIB");
591
+ if (env_small_KiB && *env_small_KiB) {
592
+ errno = 0;
593
+ long long env_small_KiB_i = strtoll(env_small_KiB, nullptr, 10);
594
+ if (errno == 0) {
595
+ small_KiB = env_small_KiB_i;
596
+ }
597
+ }
598
+ small_KiB = std::max(1LL, small_KiB);
599
+ bool no_dbi = sqlite3_uri_boolean(zName, "web_nodbi", 0);
600
+ const char *env_nodbi = getenv("SQLITE_WEB_NODBI");
601
+ if (env_nodbi && *env_nodbi) {
602
+ errno = 0;
603
+ unsigned long env_nodbi_i = strtoul(env_nodbi, nullptr, 10);
604
+ if (errno == 0 && env_nodbi_i > 0) {
605
+ no_dbi = true;
606
+ }
607
+ }
608
+ const char *encoded_dbi_uri =
609
+ no_dbi ? nullptr : sqlite3_uri_parameter(zName, "web_dbi_url");
610
+
611
+ SQLITE_VFS_LOG(5, "Load & init libcurl ...")
612
+ int rc = HTTP::global_init();
613
+ if (rc != CURLE_OK) {
614
+ if (rc == CURLE_NOT_BUILT_IN) {
615
+ last_error_ = "failed to load required symbols from libcurl; try upgrading libcurl";
616
+ } else {
617
+ last_error_ = "failed to load libcurl";
618
+ }
619
+ SQLITE_VFS_LOG(1, last_error_);
620
+ return SQLITE_ERROR;
621
+ }
622
+ SQLITE_VFS_LOG(4, "Load & init libcurl OK")
623
+
624
+ // get desired URI
625
+ try {
626
+ std::string uri, dbi_uri, reencoded_dbi_uri;
627
+ std::unique_ptr<HTTP::CURLpool> curlpool;
628
+ curlpool.reset(new HTTP::CURLpool(4, insecure));
629
+ auto conn = curlpool->checkout();
630
+ if (!conn->unescape(encoded_uri, uri)) {
631
+ last_error_ = "Failed percent-decoding web_url";
632
+ return SQLITE_CANTOPEN;
633
+ }
634
+ if (!no_dbi && encoded_dbi_uri && encoded_dbi_uri[0] &&
635
+ !conn->unescape(encoded_dbi_uri, dbi_uri)) {
636
+ last_error_ = "Failed percent-decoding web_dbi_url";
637
+ return SQLITE_CANTOPEN;
638
+ }
639
+
640
+ SQLITE_VFS_LOG(3, "opening " << uri)
641
+
642
+ // spawn background thread to sniff .dbi
643
+ bool dbi_explicit = !dbi_uri.empty() && !no_dbi;
644
+ if (!dbi_explicit && !no_dbi && uri.find('?') == std::string::npos) {
645
+ dbi_uri = uri + ".dbi";
646
+ }
647
+ std::future<int> dbi_fut;
648
+ std::unique_ptr<dbiHelper> dbi;
649
+ std::string dbi_error;
650
+ if (!dbi_uri.empty()) {
651
+ dbi_fut = std::async(std::launch::async, [&] {
652
+ return dbiHelper::Open(conn.get(), dbi_uri, insecure, log_.level(), dbi,
653
+ dbi_error);
654
+ });
655
+ }
656
+
657
+ // read main database header
658
+ unsigned long long file_size = 0, page_size = 0;
659
+ std::string db_header;
660
+ rc = FetchDatabaseHeader(log_, uri, curlpool.get(), file_size, page_size, db_header);
661
+ if (rc != SQLITE_OK) {
662
+ return rc;
663
+ }
664
+ // WebVFS::File logic assumes small extent is at least 1 page
665
+ small_KiB = std::max(1LL, std::max((long long)page_size / 1024LL, small_KiB));
666
+
667
+ // collect result of .dbi sniff
668
+ int dbi_rc = SQLITE_NOTFOUND;
669
+ if (dbi_fut.valid() && (dbi_rc = dbi_fut.get()) == SQLITE_OK) {
670
+ // verify header match between main database & .dbi
671
+ assert(dbi);
672
+ std::string dbi_header;
673
+ dbi_rc = dbi->MainDatabaseHeader(dbi_header);
674
+ if (dbi_rc == SQLITE_OK && dbi_header != db_header) {
675
+ dbi_rc = SQLITE_CORRUPT;
676
+ dbi_error = ".dbi does not match main database file";
677
+ SQLITE_VFS_LOG(2, "[" << FileNameForLog(uri) << "] " << dbi_error << ": "
678
+ << dbi_uri)
679
+ }
680
+ }
681
+ if (dbi_rc != SQLITE_OK) {
682
+ dbi.reset();
683
+ if (dbi_error.empty()) {
684
+ dbi_error = sqlite3_errstr(dbi_rc);
685
+ }
686
+ if (!no_dbi && ((dbi_explicit && log_.level() >= 2) || log_.level() >= 3)) {
687
+ SQLITE_VFS_LOG(2, "[" << FileNameForLog(uri) << "] opened without .dbi ("
688
+ << dbi_error << ")")
689
+ }
690
+ } else {
691
+ SQLITE_VFS_LOG(3, "[" << FileNameForLog(uri) << "] opened with .dbi")
692
+ }
693
+ curlpool->checkin(conn);
694
+
695
+ // Instantiate WebFile; caller will be responsible for calling xClose() on it, which
696
+ // will make it self-delete.
697
+ auto webfile = new File(zName, uri, FileNameForLog(uri), file_size, std::move(curlpool),
698
+ std::move(dbi), size_t(small_KiB));
699
+ webfile->InitHandle(pFile);
700
+ *pOutFlags = flags;
701
+ return SQLITE_OK;
702
+ } catch (std::bad_alloc &) {
703
+ return SQLITE_IOERR_NOMEM;
704
+ }
705
+ }
706
+
707
+ int GetLastError(int nByte, char *zErrMsg) override {
708
+ if (nByte && last_error_.size()) {
709
+ strncpy(zErrMsg, last_error_.c_str(), nByte);
710
+ zErrMsg[nByte - 1] = 0;
711
+ return SQLITE_OK;
712
+ }
713
+ return SQLiteVFS::Wrapper::GetLastError(nByte, zErrMsg);
714
+ }
715
+
716
+ std::string FileNameForLog(const std::string &uri) {
717
+ std::string ans = uri;
718
+ auto p = ans.find('?');
719
+ if (p != std::string::npos) {
720
+ ans = ans.substr(0, p);
721
+ }
722
+ p = ans.rfind('/');
723
+ if (p != std::string::npos) {
724
+ ans = ans.substr(p + 1);
725
+ }
726
+ if (ans.size() > 97) {
727
+ ans = ans.substr(0, 97) + "...";
728
+ }
729
+ return ans;
730
+ }
731
+
732
+ int FetchDatabaseHeader(SQLiteVFS::Logger &log_, const std::string &uri,
733
+ HTTP::CURLpool *connpool, unsigned long long &db_file_size,
734
+ unsigned long long &page_size, std::string &db_header) {
735
+ // GET range: bytes=0-99 to read the database file's header and detect its size.
736
+ db_file_size = page_size = 0;
737
+ db_header.clear();
738
+ Timer t;
739
+ const std::string protocol = uri.substr(0, 6) == "https:" ? "HTTPS" : "HTTP",
740
+ filename = FileNameForLog(uri);
741
+ HTTP::headers reqhdrs, reshdrs;
742
+ reqhdrs["range"] = "bytes=0-99";
743
+
744
+ SQLITE_VFS_LOG(5,
745
+ "[" << filename << "] " << protocol << " GET " << reqhdrs["range"] << " ...")
746
+
747
+ long status = -1;
748
+ HTTP::RetryOptions options;
749
+ options.connpool = connpool;
750
+ CURLcode rc = HTTP::RetryGet(uri, reqhdrs, status, reshdrs, db_header, options);
751
+ if (rc != CURLE_OK) {
752
+ last_error_ = "[" + filename + "] reading database header: ";
753
+ last_error_ += curl_easy_strerror(rc);
754
+ SQLITE_VFS_LOG(1, last_error_)
755
+ return SQLITE_IOERR_READ;
756
+ }
757
+ if (status < 200 || status >= 300) {
758
+ last_error_ = "[" + filename +
759
+ "] reading database header: error status = " + std::to_string(status);
760
+ SQLITE_VFS_LOG(1, last_error_)
761
+ return SQLITE_CANTOPEN;
762
+ }
763
+ if (db_header.size() != 100 ||
764
+ db_header.substr(0, 16) != std::string("SQLite format 3\000", 16)) {
765
+ last_error_ = "[" + filename + "] remote content isn't a SQLite3 database file";
766
+ SQLITE_VFS_LOG(1, last_error_)
767
+ return SQLITE_CORRUPT;
768
+ }
769
+
770
+ // parse content-range
771
+ auto size_it = reshdrs.find("content-range");
772
+ if (size_it != reshdrs.end()) {
773
+ SQLITE_VFS_LOG(5, "[" << filename << "] " << protocol
774
+ << " content-range: " << size_it->second)
775
+ const std::string &cr = size_it->second;
776
+ if (cr.substr(0, 6) == "bytes ") {
777
+ auto slash = cr.find('/');
778
+ if (slash != std::string::npos) {
779
+ std::string size_txt = cr.substr(slash + 1);
780
+ const char *size_str = size_txt.c_str();
781
+ char *endptr = nullptr;
782
+ errno = 0;
783
+ db_file_size = strtoull(size_str, &endptr, 10);
784
+ if (errno || endptr != size_str + size_txt.size()) {
785
+ db_file_size = 0;
786
+ }
787
+ }
788
+ }
789
+ }
790
+ if (!db_file_size) {
791
+ last_error_ = "[" + filename + "] " + protocol +
792
+ " GET bytes=0-99: empty file or invalid content-range header";
793
+ SQLITE_VFS_LOG(1, last_error_)
794
+ return SQLITE_IOERR_READ;
795
+ }
796
+
797
+ // read the page size & page count from the header; their product should equal file size.
798
+ // https://github.com/sqlite/sqlite/blob/8d889afc0d81839bde67731d14263026facc89d1/src/shell.c.in#L5451-L5485
799
+ page_size = (uint8_t(db_header[16]) << 8) + uint8_t(db_header[17]);
800
+ if (page_size == 1) {
801
+ page_size = 65536;
802
+ }
803
+ uint32_t page_count = 0;
804
+ for (int ofs = 28; ofs < 32; ++ofs) {
805
+ page_count <<= 8;
806
+ page_count += uint8_t(db_header[ofs]);
807
+ }
808
+ SQLITE_VFS_LOG(4, "[" << filename << "] database geometry detected: " << db_file_size
809
+ << " bytes = " << page_size << " bytes/page * " << page_count
810
+ << " pages (" << (t.micros() / 1000) << "ms)")
811
+ if (page_size < 512 || page_size > 65536 || page_count == 0 ||
812
+ db_file_size != page_size * page_count) {
813
+ last_error_ = "[" + filename +
814
+ "] database corrupt or truncated; content-range file size doesn't "
815
+ "match in-header database size";
816
+ SQLITE_VFS_LOG(1, last_error_)
817
+ return SQLITE_CORRUPT;
818
+ }
819
+ return SQLITE_OK;
820
+ }
821
+ };
822
+
823
+ } // namespace WebVFS