[mod] alles: schon recht brauchbar
This commit is contained in:
parent
7e3a1c1df5
commit
44fd89e06d
12 changed files with 320 additions and 152 deletions
|
@ -1,4 +1,4 @@
|
||||||
- `tools/update-plankton`
|
- `tools/update-plankton`
|
||||||
- `tools/build`
|
- `tools/build`
|
||||||
- `/tmp/mimir/mimir -c misc/conf-ramsch.mmr.json borg-run`
|
- `/tmp/mimir/mimir -c misc/conf-ramsch.mmr.json run`
|
||||||
|
|
||||||
|
|
|
@ -20,15 +20,20 @@ namespace _mimir.conf
|
||||||
"kind": {
|
"kind": {
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["keep"]
|
"enum": ["local"]
|
||||||
},
|
},
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"directory": {
|
||||||
|
"nullable": false,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
|
"directory",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -223,12 +228,15 @@ namespace _mimir.conf
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @todo split from conf
|
||||||
*/
|
*/
|
||||||
export type type_target_parameters_keep = {
|
export type type_target_parameters_local = {
|
||||||
|
directory : string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @todo split from conf
|
||||||
*/
|
*/
|
||||||
export type type_target_parameters_borg = {
|
export type type_target_parameters_borg = {
|
||||||
repository : string;
|
repository : string;
|
||||||
|
@ -237,6 +245,7 @@ namespace _mimir.conf
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @todo split from conf
|
||||||
*/
|
*/
|
||||||
export type type_concern_parameters_files = {
|
export type type_concern_parameters_files = {
|
||||||
name : string;
|
name : string;
|
||||||
|
@ -245,6 +254,23 @@ namespace _mimir.conf
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @todo split from conf
|
||||||
|
*/
|
||||||
|
export type type_target = (
|
||||||
|
{
|
||||||
|
kind : "local";
|
||||||
|
parameters : type_target_parameters_local;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
kind : "borg";
|
||||||
|
parameters : type_target_parameters_borg;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo split from conf
|
||||||
*/
|
*/
|
||||||
export type type_concern_parameters_postgresql_dump = {
|
export type type_concern_parameters_postgresql_dump = {
|
||||||
credentials : {
|
credentials : {
|
||||||
|
@ -258,26 +284,9 @@ namespace _mimir.conf
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @todo split from conf
|
||||||
*/
|
*/
|
||||||
export type type_conf = {
|
export type type_concern = (
|
||||||
target : (
|
|
||||||
{
|
|
||||||
kind : "keep";
|
|
||||||
parameters : type_target_parameters_keep;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
{
|
|
||||||
kind : "borg";
|
|
||||||
parameters : type_target_parameters_borg;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
concerns : Array<
|
|
||||||
{
|
|
||||||
active ?: boolean;
|
|
||||||
name : string;
|
|
||||||
}
|
|
||||||
&
|
|
||||||
(
|
|
||||||
{
|
{
|
||||||
kind : "files";
|
kind : "files";
|
||||||
parameters : type_concern_parameters_files;
|
parameters : type_concern_parameters_files;
|
||||||
|
@ -287,7 +296,20 @@ namespace _mimir.conf
|
||||||
kind : "postgresql_dump";
|
kind : "postgresql_dump";
|
||||||
parameters : type_concern_parameters_postgresql_dump;
|
parameters : type_concern_parameters_postgresql_dump;
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
export type type_conf = {
|
||||||
|
target : type_target;
|
||||||
|
concerns : Array<
|
||||||
|
{
|
||||||
|
active ?: boolean;
|
||||||
|
name : string;
|
||||||
|
}
|
||||||
|
&
|
||||||
|
type_concern
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
15
source/logic/serialization/_abstract.ts
Normal file
15
source/logic/serialization/_abstract.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
namespace _mimir.serialization
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
export type type_logic = {
|
||||||
|
execute : (
|
||||||
|
(directory : string)
|
||||||
|
=>
|
||||||
|
Array<string>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
31
source/logic/serialization/_factory.ts
Normal file
31
source/logic/serialization/_factory.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
namespace _mimir.serialization
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
export function get_logic(
|
||||||
|
concern : _mimir.conf.type_concern
|
||||||
|
) : type_logic
|
||||||
|
{
|
||||||
|
switch (concern.kind) {
|
||||||
|
default: {
|
||||||
|
throw (new Error("unhandled concern kind: " + concern["kind"]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "files": {
|
||||||
|
return {
|
||||||
|
"execute": (directory) => _mimir.serialization.files.execute(concern.parameters, directory),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "postgresql_dump": {
|
||||||
|
return {
|
||||||
|
"execute": (directory) => _mimir.serialization.postgresql_dump.execute(concern.parameters, directory),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
18
source/logic/transfer/_abstract.ts
Normal file
18
source/logic/transfer/_abstract.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
namespace _mimir.transfer
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
export type type_logic = {
|
||||||
|
execute : (
|
||||||
|
(
|
||||||
|
name : string,
|
||||||
|
stamp : string,
|
||||||
|
directory : string
|
||||||
|
)
|
||||||
|
=>
|
||||||
|
Array<string>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
31
source/logic/transfer/_factory.ts
Normal file
31
source/logic/transfer/_factory.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
namespace _mimir.transfer
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
export function get_logic(
|
||||||
|
target : _mimir.conf.type_target
|
||||||
|
) : type_logic
|
||||||
|
{
|
||||||
|
switch (target.kind) {
|
||||||
|
default: {
|
||||||
|
throw (new Error("unhandled transfer kind: " + target["kind"]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "local": {
|
||||||
|
return {
|
||||||
|
"execute": (name, stamp, directory) => _mimir.transfer.local.execute(target.parameters, name, stamp, directory),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "borg": {
|
||||||
|
return {
|
||||||
|
"execute": (name, stamp, directory) => _mimir.transfer.borg.execute(target.parameters, name, stamp, directory),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ namespace _mimir.transfer.borg
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @todo parametrize pruning
|
||||||
*/
|
*/
|
||||||
export function execute(
|
export function execute(
|
||||||
parameters : _mimir.conf.type_target_parameters_borg,
|
parameters : _mimir.conf.type_target_parameters_borg,
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
namespace _mimir.transfer.keep
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
export function execute(
|
|
||||||
parameters : _mimir.conf.type_target_parameters_keep,
|
|
||||||
name : string,
|
|
||||||
stamp : string,
|
|
||||||
directory : string
|
|
||||||
) : Array<string>
|
|
||||||
{
|
|
||||||
const result : Array<string> = [];
|
|
||||||
// do noting
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
43
source/logic/transfer/local.ts
Normal file
43
source/logic/transfer/local.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
namespace _mimir.transfer.local
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
export function execute(
|
||||||
|
parameters : _mimir.conf.type_target_parameters_local,
|
||||||
|
name : string,
|
||||||
|
stamp : string,
|
||||||
|
directory : string
|
||||||
|
) : Array<string>
|
||||||
|
{
|
||||||
|
const result : Array<string> = [];
|
||||||
|
const target_directory : string = lib_plankton.string.coin(
|
||||||
|
"{{base}}/{{stamp}}/{{name}}",
|
||||||
|
{
|
||||||
|
"base": parameters.directory,
|
||||||
|
"stamp": stamp,
|
||||||
|
"name": name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
result.push(
|
||||||
|
lib_plankton.string.coin(
|
||||||
|
"mkdir --parents {{directory}}",
|
||||||
|
{
|
||||||
|
"directory": target_directory,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
result.push(
|
||||||
|
lib_plankton.string.coin(
|
||||||
|
"mv {{directory_from}}/* {{directory_to}}/",
|
||||||
|
{
|
||||||
|
"directory_from": directory,
|
||||||
|
"directory_to": target_directory,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
177
source/main.ts
177
source/main.ts
|
@ -40,18 +40,22 @@ namespace _mimir
|
||||||
"description": "what to do",
|
"description": "what to do",
|
||||||
"options": (
|
"options": (
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"name": "conf-schema",
|
||||||
|
"description": "conf-schema"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "conf-expose",
|
||||||
|
"description": "conf-expose"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "init",
|
||||||
|
"description": "init"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "run",
|
"name": "run",
|
||||||
"description": "run"
|
"description": "run"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "borg-init",
|
|
||||||
"description": "borg-init"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "borg-run",
|
|
||||||
"description": "borg-run"
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
.map(
|
.map(
|
||||||
entry => lib_plankton.string.coin(
|
entry => lib_plankton.string.coin(
|
||||||
|
@ -102,38 +106,61 @@ namespace _mimir
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
switch (args["action"]) {
|
||||||
|
default: {
|
||||||
|
lib_plankton.log.error(
|
||||||
|
"invalid_action",
|
||||||
|
{
|
||||||
|
"action": args["action"],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return Promise.reject("invalid action");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "conf-schema": {
|
||||||
|
process.stdout.write(
|
||||||
|
JSON.stringify(
|
||||||
|
_mimir.conf.schema,
|
||||||
|
undefined,
|
||||||
|
"\t"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "conf-expose": {
|
||||||
|
const conf : _mimir.conf.type_conf = await lib_plankton.conf.load(_mimir.conf.schema, args["conf_path"]);
|
||||||
|
process.stdout.write(
|
||||||
|
JSON.stringify(
|
||||||
|
conf,
|
||||||
|
undefined,
|
||||||
|
"\t"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "init": {
|
||||||
|
return Promise.reject(new Error("not implemented"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "run": {
|
||||||
const conf : _mimir.conf.type_conf = await lib_plankton.conf.load(_mimir.conf.schema, args["conf_path"]);
|
const conf : _mimir.conf.type_conf = await lib_plankton.conf.load(_mimir.conf.schema, args["conf_path"]);
|
||||||
// process.stdout.write(JSON.stringify(conf, undefined, "\t"));
|
|
||||||
const stamp : string = get_stamp();
|
const stamp : string = get_stamp();
|
||||||
/**
|
/**
|
||||||
* @todo get from configuration
|
* @todo get from configuration
|
||||||
*/
|
*/
|
||||||
const base_directory : string = "/tmp/mimir";
|
const base_directory : string = "/tmp/mimir";
|
||||||
|
|
||||||
switch (args["action"]) {
|
|
||||||
case "init": {
|
|
||||||
return Promise.reject(new Error("not implemented"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "run": {
|
|
||||||
for await (const concern of conf.concerns) {
|
for await (const concern of conf.concerns) {
|
||||||
let commands : Array<string> = [];
|
let commands : Array<string> = [];
|
||||||
const commands_add : ((command : string) => void) = (command) => {
|
const commands_add : ((command : string) => void) = (command) => {
|
||||||
commands.push(command);
|
commands.push(command);
|
||||||
};
|
};
|
||||||
const commands_apply : (() => void) = () => {
|
const commands_apply : (() => void) = () => {
|
||||||
// TODO
|
|
||||||
process.stdout.write(commands.join("\n") + "\n");
|
process.stdout.write(commands.join("\n") + "\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (! concern.active) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const directory : string = (base_directory + "/" + stamp + "/" + concern.name);
|
|
||||||
|
|
||||||
// prepare
|
|
||||||
{
|
|
||||||
commands_add(
|
commands_add(
|
||||||
lib_plankton.string.coin(
|
lib_plankton.string.coin(
|
||||||
"## {{name}}\n",
|
"## {{name}}\n",
|
||||||
|
@ -144,13 +171,31 @@ namespace _mimir
|
||||||
);
|
);
|
||||||
commands_add(
|
commands_add(
|
||||||
lib_plankton.string.coin(
|
lib_plankton.string.coin(
|
||||||
"echo '-- {{name}}' > /dev/stderr",
|
"echo '-- {{name}}'",
|
||||||
{
|
{
|
||||||
"name": concern.name,
|
"name": concern.name,
|
||||||
"kind": concern.kind,
|
"kind": concern.kind,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (! concern.active) {
|
||||||
|
commands_add(
|
||||||
|
"echo '-- (skipping)'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const directory : string = lib_plankton.string.coin(
|
||||||
|
"{{base}}/{{stamp}}/{{name}}",
|
||||||
|
{
|
||||||
|
"base": base_directory,
|
||||||
|
"stamp": stamp,
|
||||||
|
"name": concern.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// preparation
|
||||||
|
{
|
||||||
commands_add(
|
commands_add(
|
||||||
lib_plankton.string.coin(
|
lib_plankton.string.coin(
|
||||||
"mkdir --parents {{directory}}",
|
"mkdir --parents {{directory}}",
|
||||||
|
@ -161,71 +206,42 @@ namespace _mimir
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// serialize
|
// serialization
|
||||||
{
|
{
|
||||||
switch (concern.kind) {
|
const serialization_logic : _mimir.serialization.type_logic = _mimir.serialization.get_logic(concern);
|
||||||
case "files": {
|
|
||||||
commands = commands.concat(
|
commands = commands.concat(
|
||||||
_mimir.serialization.files.execute(
|
serialization_logic.execute(
|
||||||
concern.parameters,
|
|
||||||
directory
|
directory
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "postgresql_dump": {
|
|
||||||
commands = commands.concat(
|
|
||||||
_mimir.serialization.postgresql_dump.execute(
|
|
||||||
concern.parameters,
|
|
||||||
directory
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw (new Error("unhandled kind: " + concern["kind"]));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// transfer
|
// transfer
|
||||||
{
|
{
|
||||||
switch (conf.target.kind) {
|
const target_logic : _mimir.transfer.type_logic = _mimir.transfer.get_logic(conf.target);
|
||||||
case "keep": {
|
|
||||||
commands = commands.concat(
|
commands = commands.concat(
|
||||||
_mimir.transfer.keep.execute(
|
target_logic.execute(
|
||||||
conf.target.parameters,
|
|
||||||
concern.name,
|
concern.name,
|
||||||
stamp,
|
stamp,
|
||||||
directory
|
directory
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "borg": {
|
|
||||||
commands = commands.concat(
|
|
||||||
_mimir.transfer.borg.execute(
|
|
||||||
conf.target.parameters,
|
|
||||||
concern.name,
|
|
||||||
stamp,
|
|
||||||
directory
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw (new Error("unhandled target kind: " + conf.target["kind"]));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean
|
// cleaning
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @todo
|
* @todo use shred?
|
||||||
*/
|
*/
|
||||||
|
commands_add(
|
||||||
|
lib_plankton.string.coin(
|
||||||
|
"rm --recursive {{path}}/*",
|
||||||
|
{
|
||||||
|
"path": base_directory,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands_add(
|
commands_add(
|
||||||
|
@ -236,13 +252,6 @@ namespace _mimir
|
||||||
);
|
);
|
||||||
commands_apply();
|
commands_apply();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve<void>(undefined);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw (new Error("invalid action: " + args["action"]));
|
|
||||||
|
|
||||||
return Promise.resolve<void>(undefined);
|
return Promise.resolve<void>(undefined);
|
||||||
break;
|
break;
|
||||||
|
@ -253,5 +262,17 @@ namespace _mimir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_mimir.main(process.argv.slice(2));
|
(
|
||||||
|
_mimir.main(process.argv.slice(2))
|
||||||
|
.then(
|
||||||
|
(result) => {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch(
|
||||||
|
(reason) => {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
|
@ -45,10 +45,14 @@ ${dir_temp}/mimir-raw.js: \
|
||||||
${dir_lib}/plankton/plankton.d.ts \
|
${dir_lib}/plankton/plankton.d.ts \
|
||||||
${dir_source}/helpers/borg.ts \
|
${dir_source}/helpers/borg.ts \
|
||||||
${dir_source}/conf.ts \
|
${dir_source}/conf.ts \
|
||||||
|
${dir_source}/logic/serialization/_abstract.ts \
|
||||||
${dir_source}/logic/serialization/files.ts \
|
${dir_source}/logic/serialization/files.ts \
|
||||||
${dir_source}/logic/serialization/postgresql_dump.ts \
|
${dir_source}/logic/serialization/postgresql_dump.ts \
|
||||||
${dir_source}/logic/transfer/keep.ts \
|
${dir_source}/logic/serialization/_factory.ts \
|
||||||
|
${dir_source}/logic/transfer/_abstract.ts \
|
||||||
|
${dir_source}/logic/transfer/local.ts \
|
||||||
${dir_source}/logic/transfer/borg.ts \
|
${dir_source}/logic/transfer/borg.ts \
|
||||||
|
${dir_source}/logic/transfer/_factory.ts \
|
||||||
${dir_source}/main.ts
|
${dir_source}/main.ts
|
||||||
@ ${cmd_log} "compile …"
|
@ ${cmd_log} "compile …"
|
||||||
@ ${cmd_mkdir} $(dir $@)
|
@ ${cmd_mkdir} $(dir $@)
|
||||||
|
|
|
@ -12,6 +12,7 @@ modules="${modules} call"
|
||||||
modules="${modules} json"
|
modules="${modules} json"
|
||||||
modules="${modules} string"
|
modules="${modules} string"
|
||||||
modules="${modules} args"
|
modules="${modules} args"
|
||||||
|
modules="${modules} log"
|
||||||
|
|
||||||
|
|
||||||
## exec
|
## exec
|
||||||
|
|
Loading…
Add table
Reference in a new issue