1 | #include <cstring>
|
2 | #include <napi.h>
|
3 | #include "macros.h"
|
4 | #include "database.h"
|
5 | #include "backup.h"
|
6 |
|
7 | using namespace node_sqlite3;
|
8 |
|
9 | Napi::Object Backup::Init(Napi::Env env, Napi::Object exports) {
|
10 | Napi::HandleScope scope(env);
|
11 |
|
12 |
|
13 | auto napi_default_method = static_cast<napi_property_attributes>(napi_writable | napi_configurable);
|
14 |
|
15 | auto t = DefineClass(env, "Backup", {
|
16 | InstanceMethod("step", &Backup::Step, napi_default_method),
|
17 | InstanceMethod("finish", &Backup::Finish, napi_default_method),
|
18 | InstanceAccessor("idle", &Backup::IdleGetter, nullptr),
|
19 | InstanceAccessor("completed", &Backup::CompletedGetter, nullptr),
|
20 | InstanceAccessor("failed", &Backup::FailedGetter, nullptr),
|
21 | InstanceAccessor("remaining", &Backup::RemainingGetter, nullptr),
|
22 | InstanceAccessor("pageCount", &Backup::PageCountGetter, nullptr),
|
23 | InstanceAccessor("retryErrors", &Backup::RetryErrorGetter, &Backup::RetryErrorSetter),
|
24 | });
|
25 |
|
26 | exports.Set("Backup", t);
|
27 | return exports;
|
28 | }
|
29 |
|
30 | void Backup::Process() {
|
31 | if (finished && !queue.empty()) {
|
32 | return CleanQueue();
|
33 | }
|
34 |
|
35 | while (inited && !locked && !queue.empty()) {
|
36 | auto call = std::move(queue.front());
|
37 | queue.pop();
|
38 |
|
39 | call->callback(call->baton);
|
40 | }
|
41 | }
|
42 |
|
43 | void Backup::Schedule(Work_Callback callback, Baton* baton) {
|
44 | if (finished) {
|
45 | queue.emplace(new Call(callback, baton));
|
46 | CleanQueue();
|
47 | }
|
48 | else if (!inited || locked || !queue.empty()) {
|
49 | queue.emplace(new Call(callback, baton));
|
50 | }
|
51 | else {
|
52 | callback(baton);
|
53 | }
|
54 | }
|
55 |
|
56 | template <class T> void Backup::Error(T* baton) {
|
57 | auto env = baton->backup->Env();
|
58 | Napi::HandleScope scope(env);
|
59 |
|
60 | Backup* backup = baton->backup;
|
61 |
|
62 | assert(backup->status != 0);
|
63 | EXCEPTION(Napi::String::New(env, backup->message), backup->status, exception);
|
64 |
|
65 | Napi::Function cb = baton->callback.Value();
|
66 |
|
67 | if (!cb.IsEmpty() && cb.IsFunction()) {
|
68 | Napi::Value argv[] = { exception };
|
69 | TRY_CATCH_CALL(backup->Value(), cb, 1, argv);
|
70 | }
|
71 | else {
|
72 | Napi::Value argv[] = { Napi::String::New(env, "error"), exception };
|
73 | EMIT_EVENT(backup->Value(), 2, argv);
|
74 | }
|
75 | }
|
76 |
|
77 | void Backup::CleanQueue() {
|
78 | auto env = this->Env();
|
79 | Napi::HandleScope scope(env);
|
80 |
|
81 | if (inited && !queue.empty()) {
|
82 |
|
83 |
|
84 | EXCEPTION(Napi::String::New(env, "Backup is already finished"), SQLITE_MISUSE, exception);
|
85 | Napi::Value argv[] = { exception };
|
86 | bool called = false;
|
87 |
|
88 |
|
89 | while (!queue.empty()) {
|
90 | auto call = std::move(queue.front());
|
91 | queue.pop();
|
92 |
|
93 | std::unique_ptr<Baton> baton(call->baton);
|
94 | Napi::Function cb = baton->callback.Value();
|
95 |
|
96 | if (inited && !cb.IsEmpty() &&
|
97 | cb.IsFunction()) {
|
98 | TRY_CATCH_CALL(Value(), cb, 1, argv);
|
99 | called = true;
|
100 | }
|
101 | }
|
102 |
|
103 |
|
104 |
|
105 | if (!called) {
|
106 | Napi::Value info[] = { Napi::String::New(env, "error"), exception };
|
107 | EMIT_EVENT(Value(), 2, info);
|
108 | }
|
109 | }
|
110 | else while (!queue.empty()) {
|
111 |
|
112 |
|
113 | auto call = std::move(queue.front());
|
114 | queue.pop();
|
115 |
|
116 |
|
117 |
|
118 | delete call->baton;
|
119 | }
|
120 | }
|
121 |
|
122 | Backup::Backup(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Backup>(info) {
|
123 | auto env = info.Env();
|
124 | if (!info.IsConstructCall()) {
|
125 | Napi::TypeError::New(env, "Use the new operator to create new Backup objects").ThrowAsJavaScriptException();
|
126 | return;
|
127 | }
|
128 |
|
129 | auto length = info.Length();
|
130 |
|
131 | if (length <= 0 || !Database::HasInstance(info[0])) {
|
132 | Napi::TypeError::New(env, "Database object expected").ThrowAsJavaScriptException();
|
133 | return;
|
134 | }
|
135 | else if (length <= 1 || !info[1].IsString()) {
|
136 | Napi::TypeError::New(env, "Filename expected").ThrowAsJavaScriptException();
|
137 | return;
|
138 | }
|
139 | else if (length <= 2 || !info[2].IsString()) {
|
140 | Napi::TypeError::New(env, "Source database name expected").ThrowAsJavaScriptException();
|
141 | return;
|
142 | }
|
143 | else if (length <= 3 || !info[3].IsString()) {
|
144 | Napi::TypeError::New(env, "Destination database name expected").ThrowAsJavaScriptException();
|
145 | return;
|
146 | }
|
147 | else if (length <= 4 || !info[4].IsBoolean()) {
|
148 | Napi::TypeError::New(env, "Direction flag expected").ThrowAsJavaScriptException();
|
149 | return;
|
150 | }
|
151 | else if (length > 5 && !info[5].IsUndefined() && !info[5].IsFunction()) {
|
152 | Napi::TypeError::New(env, "Callback expected").ThrowAsJavaScriptException();
|
153 | return;
|
154 | }
|
155 |
|
156 | this->db = Napi::ObjectWrap<Database>::Unwrap(info[0].As<Napi::Object>());
|
157 | this->db->Ref();
|
158 |
|
159 | auto filename = info[1].As<Napi::String>();
|
160 | auto sourceName = info[2].As<Napi::String>();
|
161 | auto destName = info[3].As<Napi::String>();
|
162 | auto filenameIsDest = info[4].As<Napi::Boolean>();
|
163 |
|
164 | info.This().As<Napi::Object>().DefineProperty(Napi::PropertyDescriptor::Value("filename", filename));
|
165 | info.This().As<Napi::Object>().DefineProperty(Napi::PropertyDescriptor::Value("sourceName", sourceName));
|
166 | info.This().As<Napi::Object>().DefineProperty(Napi::PropertyDescriptor::Value("destName", destName));
|
167 | info.This().As<Napi::Object>().DefineProperty(Napi::PropertyDescriptor::Value("filenameIsDest", filenameIsDest));
|
168 |
|
169 | auto* baton = new InitializeBaton(this->db, info[5].As<Napi::Function>(), this);
|
170 | baton->filename = filename.Utf8Value();
|
171 | baton->sourceName = sourceName.Utf8Value();
|
172 | baton->destName = destName.Utf8Value();
|
173 | baton->filenameIsDest = filenameIsDest.Value();
|
174 |
|
175 | this->db->Schedule(Work_BeginInitialize, baton);
|
176 | }
|
177 |
|
178 | void Backup::Work_BeginInitialize(Database::Baton* baton) {
|
179 | assert(baton->db->open);
|
180 | baton->db->pending++;
|
181 | auto env = baton->db->Env();
|
182 | CREATE_WORK("sqlite3.Backup.Initialize", Work_Initialize, Work_AfterInitialize);
|
183 | }
|
184 |
|
185 | void Backup::Work_Initialize(napi_env e, void* data) {
|
186 | BACKUP_INIT(InitializeBaton);
|
187 |
|
188 |
|
189 |
|
190 | auto* mtx = sqlite3_db_mutex(baton->db->_handle);
|
191 | sqlite3_mutex_enter(mtx);
|
192 |
|
193 | backup->status = sqlite3_open(baton->filename.c_str(), &backup->_otherDb);
|
194 |
|
195 | if (backup->status == SQLITE_OK) {
|
196 | backup->_handle = sqlite3_backup_init(
|
197 | baton->filenameIsDest ? backup->_otherDb : backup->db->_handle,
|
198 | baton->destName.c_str(),
|
199 | baton->filenameIsDest ? backup->db->_handle : backup->_otherDb,
|
200 | baton->sourceName.c_str());
|
201 | }
|
202 | backup->_destDb = baton->filenameIsDest ? backup->_otherDb : backup->db->_handle;
|
203 |
|
204 | if (backup->status != SQLITE_OK) {
|
205 | backup->message = std::string(sqlite3_errmsg(backup->_destDb));
|
206 | sqlite3_close(backup->_otherDb);
|
207 | backup->_otherDb = NULL;
|
208 | backup->_destDb = NULL;
|
209 | }
|
210 |
|
211 | sqlite3_mutex_leave(mtx);
|
212 | }
|
213 |
|
214 | void Backup::Work_AfterInitialize(napi_env e, napi_status status, void* data) {
|
215 | std::unique_ptr<InitializeBaton> baton(static_cast<InitializeBaton*>(data));
|
216 | auto* backup = baton->backup;
|
217 |
|
218 | auto env = backup->Env();
|
219 | Napi::HandleScope scope(env);
|
220 |
|
221 | if (backup->status != SQLITE_OK) {
|
222 | Error(baton.get());
|
223 | backup->FinishAll();
|
224 | }
|
225 | else {
|
226 | backup->inited = true;
|
227 | Napi::Function cb = baton->callback.Value();
|
228 | if (!cb.IsEmpty() && cb.IsFunction()) {
|
229 | Napi::Value argv[] = { env.Null() };
|
230 | TRY_CATCH_CALL(backup->Value(), cb, 1, argv);
|
231 | }
|
232 | }
|
233 | BACKUP_END();
|
234 | }
|
235 |
|
236 | Napi::Value Backup::Step(const Napi::CallbackInfo& info) {
|
237 | auto* backup = this;
|
238 | auto env = backup->Env();
|
239 |
|
240 | REQUIRE_ARGUMENT_INTEGER(0, pages);
|
241 | OPTIONAL_ARGUMENT_FUNCTION(1, callback);
|
242 |
|
243 | auto* baton = new StepBaton(backup, callback, pages);
|
244 | backup->GetRetryErrors(baton->retryErrorsSet);
|
245 | backup->Schedule(Work_BeginStep, baton);
|
246 | return info.This();
|
247 | }
|
248 |
|
249 | void Backup::Work_BeginStep(Baton* baton) {
|
250 | BACKUP_BEGIN(Step);
|
251 | }
|
252 |
|
253 | void Backup::Work_Step(napi_env e, void* data) {
|
254 | BACKUP_INIT(StepBaton);
|
255 | if (backup->_handle) {
|
256 | backup->status = sqlite3_backup_step(backup->_handle, baton->pages);
|
257 | backup->remaining = sqlite3_backup_remaining(backup->_handle);
|
258 | backup->pageCount = sqlite3_backup_pagecount(backup->_handle);
|
259 | }
|
260 | if (backup->status != SQLITE_OK) {
|
261 |
|
262 |
|
263 | #if SQLITE_VERSION_NUMBER >= 3007015
|
264 |
|
265 | backup->message = std::string(sqlite3_errstr(backup->status));
|
266 | #else
|
267 | backup->message = "Sqlite error";
|
268 | #endif
|
269 | if (baton->retryErrorsSet.size() > 0) {
|
270 | if (baton->retryErrorsSet.find(backup->status) == baton->retryErrorsSet.end()) {
|
271 | backup->FinishSqlite();
|
272 | }
|
273 | }
|
274 | }
|
275 | }
|
276 |
|
277 | void Backup::Work_AfterStep(napi_env e, napi_status status, void* data) {
|
278 | std::unique_ptr<StepBaton> baton(static_cast<StepBaton*>(data));
|
279 | auto* backup = baton->backup;
|
280 |
|
281 | auto env = backup->Env();
|
282 | Napi::HandleScope scope(env);
|
283 |
|
284 | if (backup->status == SQLITE_DONE) {
|
285 | backup->completed = true;
|
286 | } else if (!backup->_handle) {
|
287 | backup->failed = true;
|
288 | }
|
289 |
|
290 | if (backup->status != SQLITE_OK && backup->status != SQLITE_DONE) {
|
291 | Error(baton.get());
|
292 | }
|
293 | else {
|
294 |
|
295 | Napi::Function cb = baton->callback.Value();
|
296 | if (!cb.IsEmpty() && cb.IsFunction()) {
|
297 | Napi::Value argv[] = { env.Null(), Napi::Boolean::New(env, backup->status == SQLITE_DONE) };
|
298 | TRY_CATCH_CALL(backup->Value(), cb, 2, argv);
|
299 | }
|
300 | }
|
301 |
|
302 | BACKUP_END();
|
303 | }
|
304 |
|
305 | Napi::Value Backup::Finish(const Napi::CallbackInfo& info) {
|
306 | auto* backup = this;
|
307 | auto env = backup->Env();
|
308 |
|
309 | OPTIONAL_ARGUMENT_FUNCTION(0, callback);
|
310 |
|
311 | auto* baton = new Baton(backup, callback);
|
312 | backup->Schedule(Work_BeginFinish, baton);
|
313 | return info.This();
|
314 | }
|
315 |
|
316 | void Backup::Work_BeginFinish(Baton* baton) {
|
317 | BACKUP_BEGIN(Finish);
|
318 | }
|
319 |
|
320 | void Backup::Work_Finish(napi_env e, void* data) {
|
321 | BACKUP_INIT(Baton);
|
322 | backup->FinishSqlite();
|
323 | }
|
324 |
|
325 | void Backup::Work_AfterFinish(napi_env e, napi_status status, void* data) {
|
326 | std::unique_ptr<Baton> baton(static_cast<Baton*>(data));
|
327 | auto* backup = baton->backup;
|
328 |
|
329 | auto env = backup->Env();
|
330 | Napi::HandleScope scope(env);
|
331 |
|
332 | backup->FinishAll();
|
333 |
|
334 |
|
335 | Napi::Function cb = baton->callback.Value();
|
336 | if (!cb.IsEmpty() && cb.IsFunction()) {
|
337 | TRY_CATCH_CALL(backup->Value(), cb, 0, NULL);
|
338 | }
|
339 |
|
340 | BACKUP_END();
|
341 | }
|
342 |
|
343 | void Backup::FinishAll() {
|
344 | assert(!finished);
|
345 | if (!completed && !failed) {
|
346 | failed = true;
|
347 | }
|
348 | finished = true;
|
349 | CleanQueue();
|
350 | FinishSqlite();
|
351 | db->Unref();
|
352 | }
|
353 |
|
354 | void Backup::FinishSqlite() {
|
355 | if (_handle) {
|
356 | sqlite3_backup_finish(_handle);
|
357 | _handle = NULL;
|
358 | }
|
359 | if (_otherDb) {
|
360 | sqlite3_close(_otherDb);
|
361 | _otherDb = NULL;
|
362 | }
|
363 | _destDb = NULL;
|
364 | }
|
365 |
|
366 | Napi::Value Backup::IdleGetter(const Napi::CallbackInfo& info) {
|
367 | auto* backup = this;
|
368 | bool idle = backup->inited && !backup->locked && backup->queue.empty();
|
369 | return Napi::Boolean::New(this->Env(), idle);
|
370 | }
|
371 |
|
372 | Napi::Value Backup::CompletedGetter(const Napi::CallbackInfo& info) {
|
373 | auto* backup = this;
|
374 | return Napi::Boolean::New(this->Env(), backup->completed);
|
375 | }
|
376 |
|
377 | Napi::Value Backup::FailedGetter(const Napi::CallbackInfo& info) {
|
378 | auto* backup = this;
|
379 | return Napi::Boolean::New(this->Env(), backup->failed);
|
380 | }
|
381 |
|
382 | Napi::Value Backup::RemainingGetter(const Napi::CallbackInfo& info) {
|
383 | auto* backup = this;
|
384 | return Napi::Number::New(this->Env(), backup->remaining);
|
385 | }
|
386 |
|
387 | Napi::Value Backup::PageCountGetter(const Napi::CallbackInfo& info) {
|
388 | auto* backup = this;
|
389 | return Napi::Number::New(this->Env(), backup->pageCount);
|
390 | }
|
391 |
|
392 | Napi::Value Backup::RetryErrorGetter(const Napi::CallbackInfo& info) {
|
393 | auto* backup = this;
|
394 | return backup->retryErrors.Value();
|
395 | }
|
396 |
|
397 | void Backup::RetryErrorSetter(const Napi::CallbackInfo& info, const Napi::Value& value) {
|
398 | auto* backup = this;
|
399 | auto env = backup->Env();
|
400 | if (!value.IsArray()) {
|
401 | Napi::Error::New(env, "retryErrors must be an array").ThrowAsJavaScriptException();
|
402 | return;
|
403 | }
|
404 | Napi::Array array = value.As<Napi::Array>();
|
405 | backup->retryErrors.Reset(array, 1);
|
406 | }
|
407 |
|
408 | void Backup::GetRetryErrors(std::set<int>& retryErrorsSet) {
|
409 | retryErrorsSet.clear();
|
410 | Napi::Array array = retryErrors.Value();
|
411 | auto length = array.Length();
|
412 | for (size_t i = 0; i < length; i++) {
|
413 | Napi::Value code = (array).Get(static_cast<uint32_t>(i));
|
414 | if (code.IsNumber()) {
|
415 | retryErrorsSet.insert(code.As<Napi::Number>().Int32Value());
|
416 | }
|
417 | }
|
418 | }
|