mirror of
https://github.com/denoland/deno.git
synced 2024-11-25 15:29:32 -05:00
Implemented deno.env and refactored flags.rs
This commit is contained in:
parent
45dafe15ee
commit
f131445a46
8 changed files with 206 additions and 46 deletions
|
@ -1,6 +1,7 @@
|
||||||
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
|
||||||
// Public deno module.
|
// Public deno module.
|
||||||
export {
|
export {
|
||||||
|
env,
|
||||||
exit,
|
exit,
|
||||||
FileInfo,
|
FileInfo,
|
||||||
makeTempDirSync,
|
makeTempDirSync,
|
||||||
|
|
58
js/os.ts
58
js/os.ts
|
@ -128,6 +128,64 @@ export function readFileSync(filename: string): Uint8Array {
|
||||||
return new Uint8Array(dataArray!);
|
return new Uint8Array(dataArray!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createEnv(_msg: fbs.EnvironRes): { [index:string]: string } {
|
||||||
|
const env: { [index:string]: string } = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < _msg.mapLength(); i++) {
|
||||||
|
const item = _msg.map(i)!;
|
||||||
|
|
||||||
|
env[item.key()!] = item.value()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Proxy(env, {
|
||||||
|
set(obj, prop: string, value: string | number) {
|
||||||
|
setEnv(prop, value.toString());
|
||||||
|
return Reflect.set(obj, prop, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEnv(key: string, value: string): void {
|
||||||
|
const builder = new flatbuffers.Builder();
|
||||||
|
const _key = builder.createString(key);
|
||||||
|
const _value = builder.createString(value);
|
||||||
|
fbs.SetEnv.startSetEnv(builder);
|
||||||
|
fbs.SetEnv.addKey(builder, _key);
|
||||||
|
fbs.SetEnv.addValue(builder, _value);
|
||||||
|
const msg = fbs.SetEnv.endSetEnv(builder);
|
||||||
|
send(builder, fbs.Any.SetEnv, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a snapshot of the environment variables at invocation. Mutating a
|
||||||
|
* property in the object will set that variable in the environment for
|
||||||
|
* the process. The environment object will only accept `string`s or `number`s
|
||||||
|
* as values.
|
||||||
|
* import { env } from "deno";
|
||||||
|
* const env = deno.env();
|
||||||
|
* console.log(env.SHELL)
|
||||||
|
* env.TEST_VAR = "HELLO";
|
||||||
|
*
|
||||||
|
* const newEnv = deno.env();
|
||||||
|
* console.log(env.TEST_VAR == newEnv.TEST_VAR);
|
||||||
|
*/
|
||||||
|
export function env(): { [index:string]: string } {
|
||||||
|
/* Ideally we could write
|
||||||
|
const res = send({
|
||||||
|
command: fbs.Command.ENV,
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
const builder = new flatbuffers.Builder();
|
||||||
|
fbs.Environ.startEnviron(builder);
|
||||||
|
const msg = fbs.Environ.endEnviron(builder);
|
||||||
|
const baseRes = send(builder, fbs.Any.Environ, msg)!;
|
||||||
|
assert(fbs.Any.EnvironRes === baseRes.msgType());
|
||||||
|
const res = new fbs.EnvironRes();
|
||||||
|
assert(baseRes.msg(res) != null);
|
||||||
|
// TypeScript cannot track assertion above, therefore not null assertion
|
||||||
|
return createEnv(res);
|
||||||
|
}
|
||||||
|
|
||||||
export class FileInfo {
|
export class FileInfo {
|
||||||
private _isFile: boolean;
|
private _isFile: boolean;
|
||||||
private _isSymlink: boolean;
|
private _isSymlink: boolean;
|
||||||
|
|
|
@ -2,6 +2,27 @@
|
||||||
import { test, testPerm, assert, assertEqual } from "./test_util.ts";
|
import { test, testPerm, assert, assertEqual } from "./test_util.ts";
|
||||||
import * as deno from "deno";
|
import * as deno from "deno";
|
||||||
|
|
||||||
|
testPerm({ env: true }, async function envSuccess() {
|
||||||
|
const env = deno.env();
|
||||||
|
assert(env !== null);
|
||||||
|
env.test_var = "Hello World";
|
||||||
|
const newEnv = deno.env();
|
||||||
|
assertEqual(env.test_var, newEnv.test_var);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(async function envFailure() {
|
||||||
|
let caughtError = false;
|
||||||
|
try {
|
||||||
|
const env = deno.env();
|
||||||
|
} catch (err) {
|
||||||
|
caughtError = true;
|
||||||
|
// TODO assert(err instanceof deno.PermissionDenied).
|
||||||
|
assertEqual(err.name, "deno.PermissionDenied");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(caughtError);
|
||||||
|
});
|
||||||
|
|
||||||
// TODO Add tests for modified, accessed, and created fields once there is a way
|
// TODO Add tests for modified, accessed, and created fields once there is a way
|
||||||
// to create temp files.
|
// to create temp files.
|
||||||
test(async function statSyncSuccess() {
|
test(async function statSyncSuccess() {
|
||||||
|
|
|
@ -17,23 +17,26 @@ testing.setFilter(deno.argv[1]);
|
||||||
interface DenoPermissions {
|
interface DenoPermissions {
|
||||||
write?: boolean;
|
write?: boolean;
|
||||||
net?: boolean;
|
net?: boolean;
|
||||||
|
env?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function permToString(perms: DenoPermissions): string {
|
function permToString(perms: DenoPermissions): string {
|
||||||
const w = perms.write ? 1 : 0;
|
const w = perms.write ? 1 : 0;
|
||||||
const n = perms.net ? 1 : 0;
|
const n = perms.net ? 1 : 0;
|
||||||
return `permW${w}N${n}`;
|
const e = perms.env ? 1 : 0;
|
||||||
|
return `permW${w}N${n}E${e}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function permFromString(s: string): DenoPermissions {
|
function permFromString(s: string): DenoPermissions {
|
||||||
const re = /^permW([01])N([01])$/;
|
const re = /^permW([01])N([01])E([01])$/;
|
||||||
const found = s.match(re);
|
const found = s.match(re);
|
||||||
if (!found) {
|
if (!found) {
|
||||||
throw Error("Not a permission string");
|
throw Error("Not a permission string");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
write: Boolean(Number(found[1])),
|
write: Boolean(Number(found[1])),
|
||||||
net: Boolean(Number(found[2]))
|
net: Boolean(Number(found[2])),
|
||||||
|
env: Boolean(Number(found[3])),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,14 +46,16 @@ export function testPerm(perms: DenoPermissions, fn: testing.TestFunction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function test(fn: testing.TestFunction) {
|
export function test(fn: testing.TestFunction) {
|
||||||
testPerm({ write: false, net: false }, fn);
|
testPerm({ write: false, net: false, env: false }, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
test(function permSerialization() {
|
test(function permSerialization() {
|
||||||
for (let write of [true, false]) {
|
for (const write of [true, false]) {
|
||||||
for (let net of [true, false]) {
|
for (const net of [true, false]) {
|
||||||
let perms: DenoPermissions = { write, net };
|
for (const env of [true, false]) {
|
||||||
testing.assertEqual(perms, permFromString(permToString(perms)));
|
const perms: DenoPermissions = { write, net, env };
|
||||||
|
testing.assertEqual(perms, permFromString(permToString(perms)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
49
src/flags.rs
49
src/flags.rs
|
@ -11,7 +11,7 @@ macro_rules! svec {
|
||||||
($($x:expr),*) => (vec![$($x.to_string()),*]);
|
($($x:expr),*) => (vec![$($x.to_string()),*]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Default)]
|
||||||
pub struct DenoFlags {
|
pub struct DenoFlags {
|
||||||
pub help: bool,
|
pub help: bool,
|
||||||
pub log_debug: bool,
|
pub log_debug: bool,
|
||||||
|
@ -19,6 +19,7 @@ pub struct DenoFlags {
|
||||||
pub reload: bool,
|
pub reload: bool,
|
||||||
pub allow_write: bool,
|
pub allow_write: bool,
|
||||||
pub allow_net: bool,
|
pub allow_net: bool,
|
||||||
|
pub allow_env: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_usage() {
|
pub fn print_usage() {
|
||||||
|
@ -27,6 +28,7 @@ pub fn print_usage() {
|
||||||
|
|
||||||
--allow-write Allow file system write access.
|
--allow-write Allow file system write access.
|
||||||
--allow-net Allow network access.
|
--allow-net Allow network access.
|
||||||
|
--allow-env Allow environment access.
|
||||||
-v or --version Print the version.
|
-v or --version Print the version.
|
||||||
-r or --reload Reload cached remote resources.
|
-r or --reload Reload cached remote resources.
|
||||||
-D or --log-debug Log debug output.
|
-D or --log-debug Log debug output.
|
||||||
|
@ -37,14 +39,7 @@ pub fn print_usage() {
|
||||||
|
|
||||||
// Parses flags for deno. This does not do v8_set_flags() - call that separately.
|
// Parses flags for deno. This does not do v8_set_flags() - call that separately.
|
||||||
pub fn set_flags(args: Vec<String>) -> (DenoFlags, Vec<String>) {
|
pub fn set_flags(args: Vec<String>) -> (DenoFlags, Vec<String>) {
|
||||||
let mut flags = DenoFlags {
|
let mut flags = DenoFlags::default();
|
||||||
help: false,
|
|
||||||
version: false,
|
|
||||||
reload: false,
|
|
||||||
log_debug: false,
|
|
||||||
allow_write: false,
|
|
||||||
allow_net: false,
|
|
||||||
};
|
|
||||||
let mut rest = Vec::new();
|
let mut rest = Vec::new();
|
||||||
for a in &args {
|
for a in &args {
|
||||||
match a.as_str() {
|
match a.as_str() {
|
||||||
|
@ -52,6 +47,7 @@ pub fn set_flags(args: Vec<String>) -> (DenoFlags, Vec<String>) {
|
||||||
"-D" | "--log-debug" => flags.log_debug = true,
|
"-D" | "--log-debug" => flags.log_debug = true,
|
||||||
"-v" | "--version" => flags.version = true,
|
"-v" | "--version" => flags.version = true,
|
||||||
"-r" | "--reload" => flags.reload = true,
|
"-r" | "--reload" => flags.reload = true,
|
||||||
|
"--allow-env" => flags.allow_env = true,
|
||||||
"--allow-write" => flags.allow_write = true,
|
"--allow-write" => flags.allow_write = true,
|
||||||
"--allow-net" => flags.allow_net = true,
|
"--allow-net" => flags.allow_net = true,
|
||||||
_ => rest.push(a.clone()),
|
_ => rest.push(a.clone()),
|
||||||
|
@ -64,15 +60,10 @@ pub fn set_flags(args: Vec<String>) -> (DenoFlags, Vec<String>) {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_set_flags_1() {
|
fn test_set_flags_1() {
|
||||||
let (flags, rest) = set_flags(svec!["deno", "--version"]);
|
let (flags, rest) = set_flags(svec!["deno", "--version"]);
|
||||||
assert!(rest == svec!["deno"]);
|
assert_eq!(rest, svec!["deno"]);
|
||||||
assert!(
|
assert_eq!(flags, DenoFlags {
|
||||||
flags == DenoFlags {
|
|
||||||
help: false,
|
|
||||||
log_debug: false,
|
|
||||||
version: true,
|
version: true,
|
||||||
reload: false,
|
..DenoFlags::default()
|
||||||
allow_write: false,
|
|
||||||
allow_net: false,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -80,15 +71,11 @@ fn test_set_flags_1() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_set_flags_2() {
|
fn test_set_flags_2() {
|
||||||
let (flags, rest) = set_flags(svec!["deno", "-r", "-D", "script.ts"]);
|
let (flags, rest) = set_flags(svec!["deno", "-r", "-D", "script.ts"]);
|
||||||
assert!(rest == svec!["deno", "script.ts"]);
|
assert_eq!(rest, svec!["deno", "script.ts"]);
|
||||||
assert!(
|
assert_eq!(flags, DenoFlags {
|
||||||
flags == DenoFlags {
|
|
||||||
help: false,
|
|
||||||
log_debug: true,
|
log_debug: true,
|
||||||
version: false,
|
|
||||||
reload: true,
|
reload: true,
|
||||||
allow_write: false,
|
..DenoFlags::default()
|
||||||
allow_net: false,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -97,15 +84,11 @@ fn test_set_flags_2() {
|
||||||
fn test_set_flags_3() {
|
fn test_set_flags_3() {
|
||||||
let (flags, rest) =
|
let (flags, rest) =
|
||||||
set_flags(svec!["deno", "-r", "script.ts", "--allow-write"]);
|
set_flags(svec!["deno", "-r", "script.ts", "--allow-write"]);
|
||||||
assert!(rest == svec!["deno", "script.ts"]);
|
assert_eq!(rest, svec!["deno", "script.ts"]);
|
||||||
assert!(
|
assert_eq!(flags, DenoFlags {
|
||||||
flags == DenoFlags {
|
|
||||||
help: false,
|
|
||||||
log_debug: false,
|
|
||||||
version: false,
|
|
||||||
reload: true,
|
reload: true,
|
||||||
allow_write: true,
|
allow_write: true,
|
||||||
allow_net: false,
|
..DenoFlags::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -142,13 +125,13 @@ fn parse_core_args(args: Vec<String>) -> (Vec<String>, Vec<String>) {
|
||||||
fn test_parse_core_args_1() {
|
fn test_parse_core_args_1() {
|
||||||
let js_args =
|
let js_args =
|
||||||
parse_core_args(vec!["deno".to_string(), "--v8-options".to_string()]);
|
parse_core_args(vec!["deno".to_string(), "--v8-options".to_string()]);
|
||||||
assert!(js_args == (vec!["deno".to_string(), "--help".to_string()], vec![]));
|
assert_eq!(js_args, (vec!["deno".to_string(), "--help".to_string()], vec![]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_core_args_2() {
|
fn test_parse_core_args_2() {
|
||||||
let js_args = parse_core_args(vec!["deno".to_string(), "--help".to_string()]);
|
let js_args = parse_core_args(vec!["deno".to_string(), "--help".to_string()]);
|
||||||
assert!(js_args == (vec!["deno".to_string()], vec!["--help".to_string()]));
|
assert_eq!(js_args, (vec!["deno".to_string()], vec!["--help".to_string()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the command line arguments to v8.
|
// Pass the command line arguments to v8.
|
||||||
|
|
|
@ -45,6 +45,7 @@ pub extern "C" fn msg_from_js(d: *const DenoC, buf: deno_buf) {
|
||||||
let output_code = msg.output_code().unwrap();
|
let output_code = msg.output_code().unwrap();
|
||||||
handle_code_cache(d, &mut builder, filename, source_code, output_code)
|
handle_code_cache(d, &mut builder, filename, source_code, output_code)
|
||||||
}
|
}
|
||||||
|
msg::Any::Environ => handle_env(d, &mut builder),
|
||||||
msg::Any::FetchReq => {
|
msg::Any::FetchReq => {
|
||||||
// TODO base.msg_as_FetchReq();
|
// TODO base.msg_as_FetchReq();
|
||||||
let msg = msg::FetchReq::init_from_table(base.msg().unwrap());
|
let msg = msg::FetchReq::init_from_table(base.msg().unwrap());
|
||||||
|
@ -79,6 +80,13 @@ pub extern "C" fn msg_from_js(d: *const DenoC, buf: deno_buf) {
|
||||||
let filename = msg.filename().unwrap();
|
let filename = msg.filename().unwrap();
|
||||||
handle_read_file_sync(d, &mut builder, filename)
|
handle_read_file_sync(d, &mut builder, filename)
|
||||||
}
|
}
|
||||||
|
msg::Any::SetEnv => {
|
||||||
|
// TODO base.msg_as_SetEnv();
|
||||||
|
let msg = msg::SetEnv::init_from_table(base.msg().unwrap());
|
||||||
|
let key = msg.key().unwrap();
|
||||||
|
let value = msg.value().unwrap();
|
||||||
|
handle_set_env(d, &mut builder, key, value)
|
||||||
|
}
|
||||||
msg::Any::StatSync => {
|
msg::Any::StatSync => {
|
||||||
// TODO base.msg_as_StatSync();
|
// TODO base.msg_as_StatSync();
|
||||||
let msg = msg::StatSync::init_from_table(base.msg().unwrap());
|
let msg = msg::StatSync::init_from_table(base.msg().unwrap());
|
||||||
|
@ -244,6 +252,69 @@ fn handle_code_cache(
|
||||||
Ok(null_buf()) // null response indicates success.
|
Ok(null_buf()) // null response indicates success.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_set_env(
|
||||||
|
d: *const DenoC,
|
||||||
|
_builder: &mut FlatBufferBuilder,
|
||||||
|
key: &str,
|
||||||
|
value: &str,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let deno = from_c(d);
|
||||||
|
if !deno.flags.allow_env {
|
||||||
|
let err = std::io::Error::new(
|
||||||
|
std::io::ErrorKind::PermissionDenied,
|
||||||
|
"allow_env is off.",
|
||||||
|
);
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::env::set_var(key, value);
|
||||||
|
Ok(null_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_env(
|
||||||
|
d: *const DenoC,
|
||||||
|
builder: &mut FlatBufferBuilder,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let deno = from_c(d);
|
||||||
|
if !deno.flags.allow_env {
|
||||||
|
let err = std::io::Error::new(
|
||||||
|
std::io::ErrorKind::PermissionDenied,
|
||||||
|
"allow_env is off.",
|
||||||
|
);
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let vars: Vec<_> = std::env::vars().map(|(key, value)| {
|
||||||
|
let key = builder.create_string(&key);
|
||||||
|
let value = builder.create_string(&value);
|
||||||
|
|
||||||
|
msg::EnvPair::create(builder, &msg::EnvPairArgs {
|
||||||
|
key: Some(key),
|
||||||
|
value: Some(value),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let tables = builder.create_vector_of_reverse_offsets(&vars);
|
||||||
|
|
||||||
|
let msg = msg::EnvironRes::create(
|
||||||
|
builder,
|
||||||
|
&msg::EnvironResArgs {
|
||||||
|
map: Some(tables),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(create_msg(
|
||||||
|
builder,
|
||||||
|
&msg::BaseArgs {
|
||||||
|
msg: Some(flatbuffers::Offset::new(msg.value())),
|
||||||
|
msg_type: msg::Any::EnvironRes,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_fetch_req(
|
fn handle_fetch_req(
|
||||||
d: *const DenoC,
|
d: *const DenoC,
|
||||||
_builder: &mut FlatBufferBuilder,
|
_builder: &mut FlatBufferBuilder,
|
||||||
|
|
19
src/msg.fbs
19
src/msg.fbs
|
@ -10,6 +10,8 @@ union Any {
|
||||||
TimerStart,
|
TimerStart,
|
||||||
TimerReady,
|
TimerReady,
|
||||||
TimerClear,
|
TimerClear,
|
||||||
|
Environ,
|
||||||
|
EnvironRes,
|
||||||
FetchReq,
|
FetchReq,
|
||||||
FetchRes,
|
FetchRes,
|
||||||
MakeTempDir,
|
MakeTempDir,
|
||||||
|
@ -18,6 +20,7 @@ union Any {
|
||||||
ReadFileSyncRes,
|
ReadFileSyncRes,
|
||||||
StatSync,
|
StatSync,
|
||||||
StatSyncRes,
|
StatSyncRes,
|
||||||
|
SetEnv,
|
||||||
WriteFileSync,
|
WriteFileSync,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +127,22 @@ table TimerClear {
|
||||||
id: uint;
|
id: uint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table Environ {}
|
||||||
|
|
||||||
|
table SetEnv {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
table EnvironRes {
|
||||||
|
map: [EnvPair];
|
||||||
|
}
|
||||||
|
|
||||||
|
table EnvPair {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
table FetchReq {
|
table FetchReq {
|
||||||
id: uint;
|
id: uint;
|
||||||
url: string;
|
url: string;
|
||||||
|
|
|
@ -10,12 +10,14 @@ import sys
|
||||||
# tests by the special string. permW0N0 means allow-write but not allow-net.
|
# tests by the special string. permW0N0 means allow-write but not allow-net.
|
||||||
# See js/test_util.ts for more details.
|
# See js/test_util.ts for more details.
|
||||||
def unit_tests(deno_exe):
|
def unit_tests(deno_exe):
|
||||||
run([deno_exe, "js/unit_tests.ts", "permW0N0"])
|
run([deno_exe, "js/unit_tests.ts", "permW0N0E0"])
|
||||||
run([deno_exe, "js/unit_tests.ts", "permW1N0", "--allow-write"])
|
run([deno_exe, "js/unit_tests.ts", "permW1N0E0", "--allow-write"])
|
||||||
run([deno_exe, "js/unit_tests.ts", "permW0N1", "--allow-net"])
|
run([deno_exe, "js/unit_tests.ts", "permW0N1E0", "--allow-net"])
|
||||||
|
run([deno_exe, "js/unit_tests.ts", "permW0N0E1", "--allow-env"])
|
||||||
run([
|
run([
|
||||||
deno_exe, "js/unit_tests.ts", "permW1N1", "--allow-write",
|
deno_exe, "js/unit_tests.ts", "permW1N1E1", "--allow-write",
|
||||||
"--allow-net"
|
"--allow-net",
|
||||||
|
"--allow-env",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue