Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow downloading sqlite DB #53

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/drupal-wasm-1.0.zip
*.sql
107 changes: 107 additions & 0 deletions playground/public/assets/export-db.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

use Drupal\Core\Cache\Cache;
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;

$stdErr = fopen('php://stderr', 'w');

set_error_handler(static function(...$args) use($stdErr) {
fwrite($stdErr, print_r($args,1));
});

$flavor = file_get_contents('/config/flavor.txt');
unlink('/config/flavor.txt');

$autoloader = require "/persist/$flavor/vendor/autoload.php";

$request = Request::createFromGlobals();
$kernel = DrupalKernel::createFromRequest($request, $autoloader, 'prod');
chdir($kernel->getAppRoot());
$kernel->boot();

foreach (Cache::getBins() as $cache_backend) {
$cache_backend->deleteAll();
}
\Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
$kernel->invalidateContainer();

$sql = 'PRAGMA foreign_keys=OFF;' . PHP_EOL;
$sql .= 'BEGIN TRANSACTION;' . PHP_EOL;

// adapted from https://github.com/ephestione/php-sqlite-dump/blob/master/sqlite_dump.php
$database = \Drupal::database();
$tables = $database->query('SELECT [name], [sql] FROM {sqlite_master} WHERE [type] = "table" AND [name] NOT LIKE "sqlite_%" ORDER BY name');
foreach ($tables as $table) {
$sql .= str_replace('CREATE TABLE ', 'CREATE TABLE IF NOT EXISTS ', $table->sql) . ';' . PHP_EOL;

$columns = array_map(
static fn (object $row) => "`{$row->name}`",
$database->query("PRAGMA table_info({$table->name})")->fetchAll()
);
$columns = implode(', ', $columns);
$sql .= PHP_EOL;

$rows = $database->select($table->name)->fields($table->name)->execute()->fetchAll(\PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$values = implode(
', ',
array: array_map(
static function ($value) {
if ($value === NULL) {
return 'NULL';
}
if ($value === '') {
return "''";
}
if (is_numeric($value)) {
return $value;
}
if (is_string($value)) {
// @todo: this str_contains isn't working correctly
if (str_contains(PHP_EOL, $value)) {
$value = str_replace([PHP_EOL, "\r", "\n", "\r\n"], "\\n", $value);
$value = "replace($value,'\\n',char(10))";
mglaman marked this conversation as resolved.
Show resolved Hide resolved
}
$value = str_replace(
[
"'",
// TODO there is a null byte in serialized code, replacing it breaks unserialize what is sqlite3 .dump doing?
chr(0)],
mglaman marked this conversation as resolved.
Show resolved Hide resolved
[
"''",
''
], $value
);
return "'$value'";
}
return $value;
},
array_values($row)
)
);
$sql .= "INSERT INTO {$table->name} VALUES($values);" . PHP_EOL;
}

$sql .= PHP_EOL;
}

$sql .= 'DELETE FROM sqlite_sequence;' . PHP_EOL;
$sequences = $database->query('SELECT [name], [seq] FROM sqlite_sequence');
foreach ($sequences as $sequence) {
$sql .= "INSERT INTO sqlite_sequence VALUES('{$sequence->name}',{$sequence->seq});" . PHP_EOL;
}

$indexes = $database->query('SELECT [sql] FROM {sqlite_master} WHERE [type] = "index" AND [name] NOT LIKE "sqlite_%" ORDER BY name');
foreach ($indexes as $index) {
$sql .= $index->sql . ';' . PHP_EOL;
}


$sql .= 'COMMIT;' . PHP_EOL;

// TODO support this collation somehow
// see https://www.drupal.org/project/drupal/issues/3036487
$sql = str_replace('NOCASE_UTF8', 'NOCASE', $sql);
Comment on lines +99 to +101
Copy link
Owner Author

@mglaman mglaman Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOCASE_UTF8 is added at runtime by Drupal via PDO. I don't think there is any way to support this


file_put_contents('/persist/dump.sql', $sql);
56 changes: 50 additions & 6 deletions playground/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ <h3 class="text-sm font-medium text-yellow-800">Browser compatibility warnings</
class="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
data-launch data-flavor="drupal" data-artifact="drupal-wasm-1.0.zip"
disabled>Loading...</button>
<button type="button"
class="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
data-export-db data-flavor="drupal"
hidden disabled>Export DB</button>
<button type="button"
class="rounded-md bg-red-500 px-3.5 py-2.5 text-sm font-semibold text-drupal-black shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-red-400"
data-cleanup data-flavor="drupal" hidden disabled>Reset</button>
Expand All @@ -92,6 +96,10 @@ <h3 class="text-sm font-medium text-yellow-800">Browser compatibility warnings</
class="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
data-launch data-flavor="starshot" data-artifact="drupal-starshot.zip"
disabled>Loading...</button>
<button type="button"
class="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
data-export-db data-flavor="starshot"
hidden disabled>Export DB</button>
<button type="button"
class="rounded-md bg-red-500 px-3.5 py-2.5 text-sm font-semibold text-drupal-black shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-red-400"
data-cleanup data-flavor="starshot" hidden disabled>Reset</button>
Expand Down Expand Up @@ -144,10 +152,13 @@ <h3 class="text-sm font-medium text-yellow-800">Browser compatibility warnings</
.then(checkWww => {
const flavorCleanup = document.querySelector(`[data-cleanup][data-flavor="${flavor}"]`)
const flavorLaunch = document.querySelector(`[data-launch][data-flavor="${flavor}"]`)
const flavorExportDb = document.querySelector(`[data-export-db][data-flavor="${flavor}"]`)

if (checkWww.exists) {
flavorCleanup.hidden = false
flavorCleanup.disabled = false
flavorExportDb.hidden = false
flavorExportDb.disabled = false

flavorLaunch.innerHTML = 'Launch'
} else {
Expand All @@ -163,27 +174,29 @@ <h3 class="text-sm font-medium text-yellow-800">Browser compatibility warnings</
document.querySelectorAll('[data-cleanup]').forEach(el => {
el.addEventListener('click', cleanup)
})
document.querySelectorAll('[data-export-db]').forEach(el => {
el.addEventListener('click', exportDb)
})
});

async function cleanup() {
async function cleanup(event) {
const flavor = event.target.dataset.flavor;

const flavorCleanup = document.querySelector(`[data-cleanup][data-flavor="${flavor}"]`)
const flavorLaunch = document.querySelector(`[data-launch][data-flavor="${flavor}"]`)

flavorCleanup.hidden = true;
flavorCleanup.disabled = true;

flavorCleanup.innerHTML = 'Cleaning up...'
const openDb = indexedDB.open("/persist", 21);
openDb.onsuccess = event => {
openDb.onsuccess = () => {
const db = openDb.result;
const transaction = db.transaction(["FILE_DATA"], "readwrite");
const objectStore = transaction.objectStore("FILE_DATA");
// IDBKeyRange.bound trick found at https://stackoverflow.com/a/76714057/1949744
const objectStoreRequest = objectStore.delete(IDBKeyRange.bound(`/persist/${flavor}`, `/persist/${flavor}/\uffff`));

objectStoreRequest.onsuccess = async (event) => {
objectStoreRequest.onsuccess = async () => {
db.close();
console.log('Reloading after purging data...');
await sendMessage('refresh', []);
Expand All @@ -197,7 +210,6 @@ <h3 class="text-sm font-medium text-yellow-800">Browser compatibility warnings</
async function launch(event) {
const { flavor, artifact } = event.target.dataset;

const flavorCleanup = document.querySelector(`[data-cleanup][data-flavor="${flavor}"]`)
const flavorLaunch = document.querySelector(`[data-launch][data-flavor="${flavor}"]`)

flavorLaunch.disabled = true;
Expand All @@ -213,7 +225,10 @@ <h3 class="text-sm font-medium text-yellow-800">Browser compatibility warnings</

flavorLaunch.innerHTML = 'Installing...'

const php = new PhpWeb({ sharedLibs, persist: [{ mountPath: '/persist' }, { mountPath: '/config' }] });
const php = new PhpWeb({
sharedLibs: [`php${PhpWeb.phpVersion}-zip.so`, `php${PhpWeb.phpVersion}-zlib.so`],
persist: [{mountPath: '/persist'}, {mountPath: '/config'}]
});
await php.binary;
php.addEventListener('output', event => console.log(event.detail));
php.addEventListener('error', event => console.log(event.detail));
Expand Down Expand Up @@ -250,6 +265,35 @@ <h3 class="text-sm font-medium text-yellow-800">Browser compatibility warnings</
console.log('Redirecting...');
window.location = `/cgi/${flavor}`
}

async function exportDb(event) {
event.target.disabled = true
alert('This will download a SQL database dump from your instance.')
const { flavor } = event.target.dataset;

const php = new PhpWeb({
sharedLibs: [`php${PhpWeb.phpVersion}-sqlite.so`, `php${PhpWeb.phpVersion}-pdo-sqlite.so`,],
persist: [{mountPath: '/persist'}, {mountPath: '/config'}]
});
await php.binary;

php.addEventListener('output', event => console.log(event.detail));

await sendMessage('writeFile', ['/config/flavor.txt', flavor]);

const exportDbPhpCode = await (await fetch('/assets/export-db.php')).text();
await php.run(exportDbPhpCode)

const dbContents = await sendMessage('readFile', ['/persist/dump.sql']);
const blob = new Blob([dbContents], {type: 'application/sql'})
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'drupal.sql'
link.click();
URL.revokeObjectURL(link.href);
event.target.disabled = false
setTimeout(() => sendMessage('unlink', ['/persist/dump.sql']), 0);
}
</script>
</body>

Expand Down
20 changes: 10 additions & 10 deletions playground/public/worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ const notFound = (request) => {
};

const sharedLibs = [
`php\${PHP_VERSION}-zlib.so`,
`php\${PHP_VERSION}-zip.so`,
`php\${PHP_VERSION}-iconv.so`,
`php\${PHP_VERSION}-gd.so`,
`php\${PHP_VERSION}-dom.so`,
`php\${PHP_VERSION}-mbstring.so`,
`php\${PHP_VERSION}-sqlite.so`,
`php\${PHP_VERSION}-pdo-sqlite.so`,
`php\${PHP_VERSION}-xml.so`,
`php\${PHP_VERSION}-simplexml.so`,
`php\${PHP_VERSION}-zlib.so`,
`php\${PHP_VERSION}-zip.so`,
`php\${PHP_VERSION}-iconv.so`,
`php\${PHP_VERSION}-gd.so`,
`php\${PHP_VERSION}-dom.so`,
`php\${PHP_VERSION}-mbstring.so`,
`php\${PHP_VERSION}-sqlite.so`,
`php\${PHP_VERSION}-pdo-sqlite.so`,
`php\${PHP_VERSION}-xml.so`,
`php\${PHP_VERSION}-simplexml.so`,
];

const php = new PhpCgiWorker({
Expand Down