Skip to content

Commit ea7edf4

Browse files
committed
Import/Export keys
1 parent 84cb872 commit ea7edf4

11 files changed

Lines changed: 242 additions & 12 deletions

File tree

assets/css/styles.css

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,88 @@ svg {
15291529
border: 0;
15301530
}
15311531

1532+
.file\:mr-4::-webkit-file-upload-button {
1533+
margin-right: 1rem;
1534+
}
1535+
1536+
.file\:mr-4::file-selector-button {
1537+
margin-right: 1rem;
1538+
}
1539+
1540+
.file\:rounded::-webkit-file-upload-button {
1541+
border-radius: 0.25rem;
1542+
}
1543+
1544+
.file\:rounded::file-selector-button {
1545+
border-radius: 0.25rem;
1546+
}
1547+
1548+
.file\:border-0::-webkit-file-upload-button {
1549+
border-width: 0px;
1550+
}
1551+
1552+
.file\:border-0::file-selector-button {
1553+
border-width: 0px;
1554+
}
1555+
1556+
.file\:bg-sky-500::-webkit-file-upload-button {
1557+
--tw-bg-opacity: 1;
1558+
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
1559+
}
1560+
1561+
.file\:bg-sky-500::file-selector-button {
1562+
--tw-bg-opacity: 1;
1563+
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
1564+
}
1565+
1566+
.file\:py-2::-webkit-file-upload-button {
1567+
padding-top: 0.5rem;
1568+
padding-bottom: 0.5rem;
1569+
}
1570+
1571+
.file\:py-2::file-selector-button {
1572+
padding-top: 0.5rem;
1573+
padding-bottom: 0.5rem;
1574+
}
1575+
1576+
.file\:px-4::-webkit-file-upload-button {
1577+
padding-left: 1rem;
1578+
padding-right: 1rem;
1579+
}
1580+
1581+
.file\:px-4::file-selector-button {
1582+
padding-left: 1rem;
1583+
padding-right: 1rem;
1584+
}
1585+
1586+
.file\:text-sm::-webkit-file-upload-button {
1587+
font-size: 0.875rem;
1588+
line-height: 1.25rem;
1589+
}
1590+
1591+
.file\:text-sm::file-selector-button {
1592+
font-size: 0.875rem;
1593+
line-height: 1.25rem;
1594+
}
1595+
1596+
.file\:font-semibold::-webkit-file-upload-button {
1597+
font-weight: 600;
1598+
}
1599+
1600+
.file\:font-semibold::file-selector-button {
1601+
font-weight: 600;
1602+
}
1603+
1604+
.file\:text-white::-webkit-file-upload-button {
1605+
--tw-text-opacity: 1;
1606+
color: rgb(255 255 255 / var(--tw-text-opacity));
1607+
}
1608+
1609+
.file\:text-white::file-selector-button {
1610+
--tw-text-opacity: 1;
1611+
color: rgb(255 255 255 / var(--tw-text-opacity));
1612+
}
1613+
15321614
.hover\:bg-gray-100:hover {
15331615
--tw-bg-opacity: 1;
15341616
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
@@ -1593,6 +1675,16 @@ svg {
15931675
opacity: 0.75;
15941676
}
15951677

1678+
.hover\:file\:bg-sky-600::-webkit-file-upload-button:hover {
1679+
--tw-bg-opacity: 1;
1680+
background-color: rgb(2 132 199 / var(--tw-bg-opacity));
1681+
}
1682+
1683+
.hover\:file\:bg-sky-600::file-selector-button:hover {
1684+
--tw-bg-opacity: 1;
1685+
background-color: rgb(2 132 199 / var(--tw-bg-opacity));
1686+
}
1687+
15961688
.focus\:border-slate-300:focus {
15971689
--tw-border-opacity: 1;
15981690
border-color: rgb(203 213 225 / var(--tw-border-opacity));
@@ -1753,14 +1845,14 @@ svg {
17531845
justify-content: center;
17541846
}
17551847

1756-
.md\:gap-4 {
1757-
gap: 1rem;
1758-
}
1759-
17601848
.md\:gap-2 {
17611849
gap: 0.5rem;
17621850
}
17631851

1852+
.md\:gap-4 {
1853+
gap: 1rem;
1854+
}
1855+
17641856
.md\:p-7 {
17651857
padding: 1.75rem;
17661858
}

assets/icons/export.svg

Lines changed: 3 additions & 0 deletions
Loading

assets/icons/import.svg

Lines changed: 3 additions & 0 deletions
Loading

assets/js/scripts.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,13 @@ scripts.forEach(script => {
223223
});
224224
}
225225
});
226+
227+
/**
228+
* Import form
229+
*/
230+
let import_btn = document.getElementById('import_btn');
231+
if (import_btn) {
232+
import_btn.addEventListener('click', () => {
233+
document.getElementById('import_form').classList.toggle('hidden');
234+
});
235+
}

src/Dashboards/Memcached/MemcachedTrait.php

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ private function getAllKeys($memcached): array {
131131
$keys[] = [
132132
'key' => $key_data['key'],
133133
'ttl' => $key_data['exp'],
134-
'type' => 'string', // In Memcache(d) everything is stored as string.
134+
'type' => 'string', // In Memcache(d) everything is stored as a string.
135135
];
136136
}
137137

@@ -148,6 +148,10 @@ private function getAllKeys($memcached): array {
148148
private function mainDashboard($memcached): string {
149149
$keys = $this->getAllKeys($memcached);
150150

151+
if (isset($_POST['submit_import_key'])) {
152+
$this->import($memcached);
153+
}
154+
151155
$paginator = new Paginator($this->template, $keys);
152156

153157
return $this->template->render('dashboards/memcached/memcached', [
@@ -174,22 +178,51 @@ private function viewKey($memcached): string {
174178
Http::redirect();
175179
}
176180

181+
if (isset($_GET['export'])) {
182+
header('Content-disposition: attachment; filename='.$key.'.txt');
183+
header('Content-Type: text/plain');
184+
echo $memcached->get($key);
185+
exit;
186+
}
187+
177188
$value = $memcached->get($key);
178189

179190
[$value, $encode_fn, $is_formatted] = Helpers::decodeAndFormatValue($value);
180191

181192
$ttl = Http::get('ttl', 'int');
182193

183194
return $this->template->render('partials/view_key', [
184-
'value' => $value,
185-
'type' => 'string', // In Memcache(d) everything is stored as string.
186-
'ttl' => !empty($ttl) ? Helpers::formatSeconds($ttl) : null,
187-
'encode_fn' => $encode_fn,
188-
'formatted' => $is_formatted,
189-
'edit_url' => Http::queryString(['ttl'], ['form' => 'edit', 'key' => $key]),
195+
'value' => $value,
196+
'type' => 'string', // In Memcache(d) everything is stored as a string.
197+
'ttl' => !empty($ttl) ? Helpers::formatSeconds($ttl) : null,
198+
'encode_fn' => $encode_fn,
199+
'formatted' => $is_formatted,
200+
'edit_url' => Http::queryString(['ttl'], ['form' => 'edit', 'key' => $key]),
201+
'export_url' => Http::queryString(['ttl', 'view', 'p', 'key'], ['export' => 'key']),
190202
]);
191203
}
192204

205+
/**
206+
* Import key.
207+
*
208+
* @param Memcache|Memcached $memcached
209+
*
210+
* @return void
211+
*/
212+
private function import($memcached): void {
213+
if ($_FILES['import']['type'] === 'text/plain') {
214+
$key_name = Http::post('key_name');
215+
216+
if (empty($memcached->get($key_name))) {
217+
$value = file_get_contents($_FILES['import']['tmp_name']);
218+
219+
$memcached->store($key_name, $value, Http::post('expire', 'int'));
220+
221+
Http::redirect();
222+
}
223+
}
224+
}
225+
193226
/**
194227
* Add/edit form.
195228
*

src/Dashboards/Redis/RedisTrait.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ private function getType(int $type): string {
259259
private function mainDashboard(Redis $redis): string {
260260
$keys = $this->getAllKeys($redis);
261261

262+
if (isset($_POST['submit_import_key'])) {
263+
$this->import($redis);
264+
}
265+
262266
$paginator = new Paginator($this->template, $keys);
263267
$paginator->setUrl([['db', 's', 'pp'], ['p' => '']]);
264268

@@ -294,6 +298,13 @@ private function viewKey(Redis $redis): string {
294298
$this->deleteSubKey($redis, $type, $key);
295299
}
296300

301+
if (isset($_GET['export'])) {
302+
header('Content-disposition: attachment; filename='.$key.'.bin');
303+
header('Content-Type: application/octet-stream');
304+
echo $redis->dump($key);
305+
exit;
306+
}
307+
297308
$value = $this->getAllKeyValues($redis, $type, $key);
298309

299310
$paginator = '';
@@ -333,10 +344,35 @@ private function viewKey(Redis $redis): string {
333344
'add_subkey' => Http::queryString(['db'], ['form' => 'new', 'key' => $key]),
334345
'edit_url' => Http::queryString(['db'], ['form' => 'edit', 'key' => $key]),
335346
'delete_url' => Http::queryString(['db', 'view', 'p'], ['deletesub' => 'key', 'key' => $key]),
347+
'export_url' => Http::queryString(['db', 'view', 'p', 'key'], ['export' => 'key']),
336348
'paginator' => $paginator,
337349
]);
338350
}
339351

352+
/**
353+
* Import key.
354+
*
355+
* @param Redis $redis
356+
*
357+
* @return void
358+
*/
359+
private function import(Redis $redis): void {
360+
if ($_FILES['import']['type'] === 'application/octet-stream') {
361+
$key_name = Http::post('key_name');
362+
363+
if (!$redis->exists($key_name)) {
364+
$value = file_get_contents($_FILES['import']['tmp_name']);
365+
366+
$expire = Http::post('expire', 'int');
367+
$expire = $expire === -1 ? 0 : $expire;
368+
369+
$redis->restore($key_name, $expire, $value);
370+
371+
Http::redirect(['db']);
372+
}
373+
}
374+
}
375+
340376
/**
341377
* Add/edit a form.
342378
*

templates/dashboards/memcached/memcached.twig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,10 @@
22
{{ include('partials/keys_buttons.twig') }}
33
</div>
44

5+
{{ include('partials/import_form.twig', {
6+
expire_default: '0',
7+
expire_max: 2592000,
8+
key_desc: 'The text value in the .txt file.',
9+
}) }}
10+
511
{{ include('partials/keys_table.twig') }}

templates/dashboards/redis/redis.twig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,10 @@
2121
</div>
2222
</div>
2323

24+
{{ include('partials/import_form.twig', {
25+
expire_default: '-1',
26+
expire_max: 2147483647,
27+
key_desc: 'A binary value in a .bin file.',
28+
}) }}
29+
2430
{{ include('partials/keys_table.twig') }}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<form method="post" enctype="multipart/form-data" id="import_form" class="hidden">
2+
<div class="bg-white shadow-sm shadow-gray-300 rounded p-3 mb-5">
3+
<div class="md:grid md:grid-cols-2 md:gap-2">
4+
{{ include('components/input.twig', {
5+
id: 'key_name',
6+
label: 'Key name',
7+
}) }}
8+
9+
{{ include('components/input.twig', {
10+
id: 'expire',
11+
label: 'Expire (in seconds)',
12+
type: 'number',
13+
help: expire_default ~ ' removes expiration (default).',
14+
value: expire_default,
15+
attr: ' min="' ~ expire_default ~ '" max="' ~ expire_max ~ '"',
16+
}) }}
17+
</div>
18+
19+
<div class="mb-3">
20+
<label for="import" class="block text-sm font-semibold mb-2">Key</label>
21+
<input id="import" name="import" type="file"
22+
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0
23+
file:text-sm file:font-semibold file:text-white file:bg-sky-500 hover:file:bg-sky-600">
24+
<small class="text-xs text-gray-400">{{ key_desc }}</small>
25+
</div>
26+
27+
<button type="submit" name="submit_import_key"
28+
class="text-sm py-2 px-4 text-white bg-green-500 hover:bg-green-600 focus:ring-4 focus:ring-green-300 font-semibold rounded inline-flex items-center gap-1">
29+
{{ svg('import') }} Import
30+
</button>
31+
</div>
32+
</form>

templates/partials/keys_buttons.twig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
<button type="button" id="import_btn"
2+
class="text-sm py-1.5 px-3 text-white bg-green-500 hover:bg-green-600 focus:ring-4 focus:ring-green-300 font-semibold rounded inline-flex items-center gap-1 [&>svg]:align-middle">
3+
{{ svg('import') }} Import
4+
</button>
5+
16
<a href="{{ new_key_url }}"
27
class="text-sm py-1.5 px-3 text-white bg-green-500 hover:bg-green-600 focus:ring-4 focus:ring-green-300 font-semibold rounded inline-flex items-center gap-1 [&>svg]:align-middle">
38
{{ svg('open') }} Add new key

0 commit comments

Comments
 (0)