2016-03-30 14:27:25 +03:00
|
|
|
#include "sqlite.hh"
|
2021-11-03 14:10:16 +02:00
|
|
|
#include "globals.hh"
|
2016-03-30 14:27:25 +03:00
|
|
|
#include "util.hh"
|
2023-06-20 12:34:09 +03:00
|
|
|
#include "url.hh"
|
2023-10-25 07:43:36 +03:00
|
|
|
#include "signals.hh"
|
2016-03-30 14:27:25 +03:00
|
|
|
|
|
|
|
#include <sqlite3.h>
|
|
|
|
|
2017-02-28 14:59:11 +02:00
|
|
|
#include <atomic>
|
2024-04-04 19:42:41 +03:00
|
|
|
#include <thread>
|
2017-02-28 14:59:11 +02:00
|
|
|
|
2016-03-30 14:27:25 +03:00
|
|
|
namespace nix {
|
|
|
|
|
2024-02-04 06:35:19 +02:00
|
|
|
SQLiteError::SQLiteError(const char *path, const char *errMsg, int errNo, int extendedErrNo, int offset, HintFmt && hf)
|
Improve sqlite error messages
They did not include the detailed error message, losing essential
information for troubleshooting.
Example message:
warning: creating statement 'insert or rplace into NARs(cache, hashPart, namePart, url, compression, fileHash, fileSize, narHash, narSize, refs, deriver, sigs, ca, timestamp, present) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)': at offset 10: SQL logic error, near "rplace": syntax error (in '/tmp/nix-shell.grQ6f7/nix-test/tests/binary-cache/test-home/.cache/nix/binary-cache-v6.sqlite')
It's not the best example; more important information will be in
the message for e.g. a constraint violation.
I don't see why this specific error is printed as a warning, but
that's for another commit.
2022-12-17 15:50:43 +02:00
|
|
|
: Error(""), path(path), errMsg(errMsg), errNo(errNo), extendedErrNo(extendedErrNo), offset(offset)
|
2022-04-20 20:37:59 +03:00
|
|
|
{
|
Improve sqlite error messages
They did not include the detailed error message, losing essential
information for troubleshooting.
Example message:
warning: creating statement 'insert or rplace into NARs(cache, hashPart, namePart, url, compression, fileHash, fileSize, narHash, narSize, refs, deriver, sigs, ca, timestamp, present) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)': at offset 10: SQL logic error, near "rplace": syntax error (in '/tmp/nix-shell.grQ6f7/nix-test/tests/binary-cache/test-home/.cache/nix/binary-cache-v6.sqlite')
It's not the best example; more important information will be in
the message for e.g. a constraint violation.
I don't see why this specific error is printed as a warning, but
that's for another commit.
2022-12-17 15:50:43 +02:00
|
|
|
auto offsetStr = (offset == -1) ? "" : "at offset " + std::to_string(offset) + ": ";
|
2024-02-04 06:35:19 +02:00
|
|
|
err.msg = HintFmt("%s: %s%s, %s (in '%s')",
|
2024-02-04 05:16:30 +02:00
|
|
|
Uncolored(hf.str()),
|
Improve sqlite error messages
They did not include the detailed error message, losing essential
information for troubleshooting.
Example message:
warning: creating statement 'insert or rplace into NARs(cache, hashPart, namePart, url, compression, fileHash, fileSize, narHash, narSize, refs, deriver, sigs, ca, timestamp, present) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)': at offset 10: SQL logic error, near "rplace": syntax error (in '/tmp/nix-shell.grQ6f7/nix-test/tests/binary-cache/test-home/.cache/nix/binary-cache-v6.sqlite')
It's not the best example; more important information will be in
the message for e.g. a constraint violation.
I don't see why this specific error is printed as a warning, but
that's for another commit.
2022-12-17 15:50:43 +02:00
|
|
|
offsetStr,
|
2022-04-20 20:37:59 +03:00
|
|
|
sqlite3_errstr(extendedErrNo),
|
Improve sqlite error messages
They did not include the detailed error message, losing essential
information for troubleshooting.
Example message:
warning: creating statement 'insert or rplace into NARs(cache, hashPart, namePart, url, compression, fileHash, fileSize, narHash, narSize, refs, deriver, sigs, ca, timestamp, present) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)': at offset 10: SQL logic error, near "rplace": syntax error (in '/tmp/nix-shell.grQ6f7/nix-test/tests/binary-cache/test-home/.cache/nix/binary-cache-v6.sqlite')
It's not the best example; more important information will be in
the message for e.g. a constraint violation.
I don't see why this specific error is printed as a warning, but
that's for another commit.
2022-12-17 15:50:43 +02:00
|
|
|
errMsg,
|
2022-04-20 20:37:59 +03:00
|
|
|
path ? path : "(in-memory)");
|
|
|
|
}
|
|
|
|
|
2024-02-04 06:35:19 +02:00
|
|
|
[[noreturn]] void SQLiteError::throw_(sqlite3 * db, HintFmt && hf)
|
2022-04-20 20:37:59 +03:00
|
|
|
{
|
|
|
|
int err = sqlite3_errcode(db);
|
|
|
|
int exterr = sqlite3_extended_errcode(db);
|
Improve sqlite error messages
They did not include the detailed error message, losing essential
information for troubleshooting.
Example message:
warning: creating statement 'insert or rplace into NARs(cache, hashPart, namePart, url, compression, fileHash, fileSize, narHash, narSize, refs, deriver, sigs, ca, timestamp, present) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)': at offset 10: SQL logic error, near "rplace": syntax error (in '/tmp/nix-shell.grQ6f7/nix-test/tests/binary-cache/test-home/.cache/nix/binary-cache-v6.sqlite')
It's not the best example; more important information will be in
the message for e.g. a constraint violation.
I don't see why this specific error is printed as a warning, but
that's for another commit.
2022-12-17 15:50:43 +02:00
|
|
|
int offset = sqlite3_error_offset(db);
|
2022-04-20 20:37:59 +03:00
|
|
|
|
|
|
|
auto path = sqlite3_db_filename(db, nullptr);
|
Improve sqlite error messages
They did not include the detailed error message, losing essential
information for troubleshooting.
Example message:
warning: creating statement 'insert or rplace into NARs(cache, hashPart, namePart, url, compression, fileHash, fileSize, narHash, narSize, refs, deriver, sigs, ca, timestamp, present) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)': at offset 10: SQL logic error, near "rplace": syntax error (in '/tmp/nix-shell.grQ6f7/nix-test/tests/binary-cache/test-home/.cache/nix/binary-cache-v6.sqlite')
It's not the best example; more important information will be in
the message for e.g. a constraint violation.
I don't see why this specific error is printed as a warning, but
that's for another commit.
2022-12-17 15:50:43 +02:00
|
|
|
auto errMsg = sqlite3_errmsg(db);
|
2022-04-20 20:37:59 +03:00
|
|
|
|
|
|
|
if (err == SQLITE_BUSY || err == SQLITE_PROTOCOL) {
|
Improve sqlite error messages
They did not include the detailed error message, losing essential
information for troubleshooting.
Example message:
warning: creating statement 'insert or rplace into NARs(cache, hashPart, namePart, url, compression, fileHash, fileSize, narHash, narSize, refs, deriver, sigs, ca, timestamp, present) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)': at offset 10: SQL logic error, near "rplace": syntax error (in '/tmp/nix-shell.grQ6f7/nix-test/tests/binary-cache/test-home/.cache/nix/binary-cache-v6.sqlite')
It's not the best example; more important information will be in
the message for e.g. a constraint violation.
I don't see why this specific error is printed as a warning, but
that's for another commit.
2022-12-17 15:50:43 +02:00
|
|
|
auto exp = SQLiteBusy(path, errMsg, err, exterr, offset, std::move(hf));
|
2024-02-04 06:35:19 +02:00
|
|
|
exp.err.msg = HintFmt(
|
2022-04-20 20:37:59 +03:00
|
|
|
err == SQLITE_PROTOCOL
|
|
|
|
? "SQLite database '%s' is busy (SQLITE_PROTOCOL)"
|
|
|
|
: "SQLite database '%s' is busy",
|
|
|
|
path ? path : "(in-memory)");
|
|
|
|
throw exp;
|
|
|
|
} else
|
Improve sqlite error messages
They did not include the detailed error message, losing essential
information for troubleshooting.
Example message:
warning: creating statement 'insert or rplace into NARs(cache, hashPart, namePart, url, compression, fileHash, fileSize, narHash, narSize, refs, deriver, sigs, ca, timestamp, present) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)': at offset 10: SQL logic error, near "rplace": syntax error (in '/tmp/nix-shell.grQ6f7/nix-test/tests/binary-cache/test-home/.cache/nix/binary-cache-v6.sqlite')
It's not the best example; more important information will be in
the message for e.g. a constraint violation.
I don't see why this specific error is printed as a warning, but
that's for another commit.
2022-12-17 15:50:43 +02:00
|
|
|
throw SQLiteError(path, errMsg, err, exterr, offset, std::move(hf));
|
2022-04-20 20:37:59 +03:00
|
|
|
}
|
|
|
|
|
2023-01-17 20:53:21 +02:00
|
|
|
static void traceSQL(void * x, const char * sql)
|
|
|
|
{
|
|
|
|
// wacky delimiters:
|
|
|
|
// so that we're quite unambiguous without escaping anything
|
|
|
|
// notice instead of trace:
|
|
|
|
// so that this can be enabled without getting the firehose in our face.
|
|
|
|
notice("SQL<[%1%]>", sql);
|
|
|
|
};
|
|
|
|
|
2023-06-20 12:34:09 +03:00
|
|
|
SQLite::SQLite(const Path & path, SQLiteOpenMode mode)
|
2016-08-09 15:27:30 +03:00
|
|
|
{
|
2021-11-03 14:10:16 +02:00
|
|
|
// useSQLiteWAL also indicates what virtual file system we need. Using
|
|
|
|
// `unix-dotfile` is needed on NFS file systems and on Windows' Subsystem
|
|
|
|
// for Linux (WSL) where useSQLiteWAL should be false by default.
|
|
|
|
const char *vfs = settings.useSQLiteWAL ? 0 : "unix-dotfile";
|
2023-06-20 12:34:09 +03:00
|
|
|
bool immutable = mode == SQLiteOpenMode::Immutable;
|
|
|
|
int flags = immutable ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE;
|
|
|
|
if (mode == SQLiteOpenMode::Normal) flags |= SQLITE_OPEN_CREATE;
|
|
|
|
auto uri = "file:" + percentEncode(path) + "?immutable=" + (immutable ? "1" : "0");
|
|
|
|
int ret = sqlite3_open_v2(uri.c_str(), &db, SQLITE_OPEN_URI | flags, vfs);
|
2022-12-23 02:33:32 +02:00
|
|
|
if (ret != SQLITE_OK) {
|
|
|
|
const char * err = sqlite3_errstr(ret);
|
|
|
|
throw Error("cannot open SQLite database '%s': %s", path, err);
|
|
|
|
}
|
2019-06-07 23:25:48 +03:00
|
|
|
|
|
|
|
if (sqlite3_busy_timeout(db, 60 * 60 * 1000) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(db, "setting timeout");
|
2019-06-07 23:25:48 +03:00
|
|
|
|
2023-01-17 20:53:21 +02:00
|
|
|
if (getEnv("NIX_DEBUG_SQLITE_TRACES") == "1") {
|
|
|
|
// To debug sqlite statements; trace all of them
|
|
|
|
sqlite3_trace(db, &traceSQL, nullptr);
|
|
|
|
}
|
|
|
|
|
2019-06-07 23:25:48 +03:00
|
|
|
exec("pragma foreign_keys = 1");
|
2016-08-09 15:27:30 +03:00
|
|
|
}
|
|
|
|
|
2016-03-30 14:27:25 +03:00
|
|
|
SQLite::~SQLite()
|
|
|
|
{
|
|
|
|
try {
|
|
|
|
if (db && sqlite3_close(db) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(db, "closing database");
|
2016-03-30 14:27:25 +03:00
|
|
|
} catch (...) {
|
2024-09-30 12:49:53 +03:00
|
|
|
ignoreExceptionInDestructor();
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-07 23:25:48 +03:00
|
|
|
void SQLite::isCache()
|
|
|
|
{
|
|
|
|
exec("pragma synchronous = off");
|
|
|
|
exec("pragma main.journal_mode = truncate");
|
|
|
|
}
|
|
|
|
|
2016-08-09 15:27:30 +03:00
|
|
|
void SQLite::exec(const std::string & stmt)
|
|
|
|
{
|
2017-02-28 14:59:11 +02:00
|
|
|
retrySQLite<void>([&]() {
|
|
|
|
if (sqlite3_exec(db, stmt.c_str(), 0, 0, 0) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(db, "executing SQLite statement '%s'", stmt);
|
2017-02-28 14:59:11 +02:00
|
|
|
});
|
2016-08-09 15:27:30 +03:00
|
|
|
}
|
|
|
|
|
2020-04-18 00:04:21 +03:00
|
|
|
uint64_t SQLite::getLastInsertedRowId()
|
|
|
|
{
|
|
|
|
return sqlite3_last_insert_rowid(db);
|
|
|
|
}
|
|
|
|
|
2022-02-25 17:00:00 +02:00
|
|
|
void SQLiteStmt::create(sqlite3 * db, const std::string & sql)
|
2016-03-30 14:27:25 +03:00
|
|
|
{
|
|
|
|
checkInterrupt();
|
|
|
|
assert(!stmt);
|
2017-02-28 14:59:11 +02:00
|
|
|
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(db, "creating statement '%s'", sql);
|
2016-03-30 14:27:25 +03:00
|
|
|
this->db = db;
|
2017-02-28 14:59:11 +02:00
|
|
|
this->sql = sql;
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
SQLiteStmt::~SQLiteStmt()
|
|
|
|
{
|
|
|
|
try {
|
|
|
|
if (stmt && sqlite3_finalize(stmt) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(db, "finalizing statement '%s'", sql);
|
2016-03-30 14:27:25 +03:00
|
|
|
} catch (...) {
|
2024-09-30 12:49:53 +03:00
|
|
|
ignoreExceptionInDestructor();
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-30 16:50:45 +03:00
|
|
|
SQLiteStmt::Use::Use(SQLiteStmt & stmt)
|
|
|
|
: stmt(stmt)
|
2016-03-30 14:27:25 +03:00
|
|
|
{
|
2016-03-30 16:50:45 +03:00
|
|
|
assert(stmt.stmt);
|
|
|
|
/* Note: sqlite3_reset() returns the error code for the most
|
|
|
|
recent call to sqlite3_step(). So ignore it. */
|
|
|
|
sqlite3_reset(stmt);
|
2016-04-05 16:29:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
SQLiteStmt::Use::~Use()
|
|
|
|
{
|
|
|
|
sqlite3_reset(stmt);
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|
|
|
|
|
2020-04-17 02:00:56 +03:00
|
|
|
SQLiteStmt::Use & SQLiteStmt::Use::operator () (std::string_view value, bool notNull)
|
2016-03-30 14:27:25 +03:00
|
|
|
{
|
2016-03-30 16:50:45 +03:00
|
|
|
if (notNull) {
|
2020-04-17 02:00:56 +03:00
|
|
|
if (sqlite3_bind_text(stmt, curArg++, value.data(), -1, SQLITE_TRANSIENT) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(stmt.db, "binding argument");
|
2016-03-30 16:50:45 +03:00
|
|
|
} else
|
|
|
|
bind();
|
|
|
|
return *this;
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|
|
|
|
|
2019-06-07 23:25:48 +03:00
|
|
|
SQLiteStmt::Use & SQLiteStmt::Use::operator () (const unsigned char * data, size_t len, bool notNull)
|
|
|
|
{
|
|
|
|
if (notNull) {
|
|
|
|
if (sqlite3_bind_blob(stmt, curArg++, data, len, SQLITE_TRANSIENT) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(stmt.db, "binding argument");
|
2019-06-07 23:25:48 +03:00
|
|
|
} else
|
|
|
|
bind();
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
2016-03-30 16:50:45 +03:00
|
|
|
SQLiteStmt::Use & SQLiteStmt::Use::operator () (int64_t value, bool notNull)
|
2016-03-30 14:27:25 +03:00
|
|
|
{
|
2016-03-30 16:50:45 +03:00
|
|
|
if (notNull) {
|
|
|
|
if (sqlite3_bind_int64(stmt, curArg++, value) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(stmt.db, "binding argument");
|
2016-03-30 16:50:45 +03:00
|
|
|
} else
|
|
|
|
bind();
|
|
|
|
return *this;
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|
|
|
|
|
2016-03-30 16:50:45 +03:00
|
|
|
SQLiteStmt::Use & SQLiteStmt::Use::bind()
|
2016-03-30 14:27:25 +03:00
|
|
|
{
|
|
|
|
if (sqlite3_bind_null(stmt, curArg++) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(stmt.db, "binding argument");
|
2016-03-30 16:50:45 +03:00
|
|
|
return *this;
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|
|
|
|
|
2016-03-30 16:50:45 +03:00
|
|
|
int SQLiteStmt::Use::step()
|
2016-03-30 14:27:25 +03:00
|
|
|
{
|
2016-03-30 16:50:45 +03:00
|
|
|
return sqlite3_step(stmt);
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|
|
|
|
|
2016-03-30 16:50:45 +03:00
|
|
|
void SQLiteStmt::Use::exec()
|
2016-03-30 14:27:25 +03:00
|
|
|
{
|
2016-03-30 16:50:45 +03:00
|
|
|
int r = step();
|
|
|
|
assert(r != SQLITE_ROW);
|
|
|
|
if (r != SQLITE_DONE)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(stmt.db, fmt("executing SQLite statement '%s'", sqlite3_expanded_sql(stmt.stmt)));
|
2016-03-30 16:50:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
bool SQLiteStmt::Use::next()
|
|
|
|
{
|
|
|
|
int r = step();
|
|
|
|
if (r != SQLITE_DONE && r != SQLITE_ROW)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(stmt.db, fmt("executing SQLite query '%s'", sqlite3_expanded_sql(stmt.stmt)));
|
2016-03-30 16:50:45 +03:00
|
|
|
return r == SQLITE_ROW;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string SQLiteStmt::Use::getStr(int col)
|
|
|
|
{
|
|
|
|
auto s = (const char *) sqlite3_column_text(stmt, col);
|
|
|
|
assert(s);
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
|
|
|
int64_t SQLiteStmt::Use::getInt(int col)
|
|
|
|
{
|
|
|
|
// FIXME: detect nulls?
|
|
|
|
return sqlite3_column_int64(stmt, col);
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|
|
|
|
|
2016-04-20 15:12:38 +03:00
|
|
|
bool SQLiteStmt::Use::isNull(int col)
|
|
|
|
{
|
|
|
|
return sqlite3_column_type(stmt, col) == SQLITE_NULL;
|
|
|
|
}
|
|
|
|
|
2016-03-30 14:27:25 +03:00
|
|
|
SQLiteTxn::SQLiteTxn(sqlite3 * db)
|
|
|
|
{
|
|
|
|
this->db = db;
|
|
|
|
if (sqlite3_exec(db, "begin;", 0, 0, 0) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(db, "starting transaction");
|
2016-03-30 14:27:25 +03:00
|
|
|
active = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SQLiteTxn::commit()
|
|
|
|
{
|
|
|
|
if (sqlite3_exec(db, "commit;", 0, 0, 0) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(db, "committing transaction");
|
2016-03-30 14:27:25 +03:00
|
|
|
active = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
SQLiteTxn::~SQLiteTxn()
|
|
|
|
{
|
|
|
|
try {
|
|
|
|
if (active && sqlite3_exec(db, "rollback;", 0, 0, 0) != SQLITE_OK)
|
2020-06-18 20:54:16 +03:00
|
|
|
SQLiteError::throw_(db, "aborting transaction");
|
2016-03-30 14:27:25 +03:00
|
|
|
} catch (...) {
|
2024-09-30 12:49:53 +03:00
|
|
|
ignoreExceptionInDestructor();
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-11 18:03:37 +03:00
|
|
|
void handleSQLiteBusy(const SQLiteBusy & e, time_t & nextWarning)
|
2017-02-28 14:59:11 +02:00
|
|
|
{
|
2023-04-11 18:03:37 +03:00
|
|
|
time_t now = time(0);
|
|
|
|
if (now > nextWarning) {
|
|
|
|
nextWarning = now + 10;
|
2020-06-15 15:06:58 +03:00
|
|
|
logWarning({
|
2024-02-04 06:35:19 +02:00
|
|
|
.msg = HintFmt(e.what())
|
2020-06-15 15:06:58 +03:00
|
|
|
});
|
2017-02-28 14:59:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Sleep for a while since retrying the transaction right away
|
|
|
|
is likely to fail again. */
|
|
|
|
checkInterrupt();
|
2024-04-04 19:42:41 +03:00
|
|
|
/* <= 0.1s */
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds { rand() % 100 });
|
2017-02-28 14:59:11 +02:00
|
|
|
}
|
|
|
|
|
2016-03-30 14:27:25 +03:00
|
|
|
}
|