diff --git a/.gitignore b/.gitignore
index a56201d..b7e6841 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/drupal-wasm-1.0.zip
+*.sql
diff --git a/playground/public/assets/export-db.php b/playground/public/assets/export-db.php
new file mode 100644
index 0000000..5b15fe5
--- /dev/null
+++ b/playground/public/assets/export-db.php
@@ -0,0 +1,103 @@
+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 ($value === 'b:0;') {
+ return false;
+ }
+ $is_serialized = $value[1] === ':' && str_ends_with($value, '}');
+
+ $value = str_replace(["'"], ["''"], $value);
+ $value = "'$value'";
+
+ if ($is_serialized) {
+ $value = sprintf(
+ "replace(%s, char(1), char(0))",
+ str_replace(chr(0), chr(1), $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);
+
+file_put_contents('/persist/dump.sql', $sql);
diff --git a/playground/public/index.html b/playground/public/index.html
index 4a6de98..dcab014 100644
--- a/playground/public/index.html
+++ b/playground/public/index.html
@@ -75,6 +75,10 @@
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...
+
@@ -92,6 +96,10 @@ 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...
+
@@ -144,10 +152,13 @@ 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 {
@@ -163,6 +174,9 @@ 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(event ) {
@@ -178,14 +192,14 @@ Browser compatibility warnings
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', []);
@@ -199,7 +213,6 @@ 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;
@@ -215,7 +228,10 @@ 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));
@@ -252,6 +268,35 @@ 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);
+ }