ropkg: implement create command
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
file(GLOB sources *.c *.h)
|
file(GLOB sources *.c *.h)
|
||||||
|
|
||||||
add_executable(ropkg ${sources})
|
add_executable(ropkg ${sources})
|
||||||
target_link_libraries(ropkg Bluelib::Core Bluelib::Object Bluelib::Io Bluelib::Term Bluelib::Cmd)
|
target_link_libraries(ropkg
|
||||||
|
libropkg
|
||||||
|
Bluelib::Core Bluelib::Object Bluelib::Io Bluelib::Term Bluelib::Cmd)
|
||||||
|
|||||||
464
ropkg/create.c
464
ropkg/create.c
@@ -1,6 +1,14 @@
|
|||||||
#include "commands.h"
|
#include "commands.h"
|
||||||
|
|
||||||
#include <blue/cmd.h>
|
#include <blue/cmd.h>
|
||||||
|
#include <blue/io/directory.h>
|
||||||
|
#include <blue/term/print.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <ropkg/compress.h>
|
||||||
|
#include <ropkg/stream.h>
|
||||||
|
#include <ropkg/writer.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
OPT_OUTPATH,
|
OPT_OUTPATH,
|
||||||
@@ -9,19 +17,290 @@ enum {
|
|||||||
OPT_MANIFEST,
|
OPT_MANIFEST,
|
||||||
OPT_MANIFEST_PATH,
|
OPT_MANIFEST_PATH,
|
||||||
|
|
||||||
|
OPT_NEWS,
|
||||||
|
OPT_NEWS_PATH,
|
||||||
|
|
||||||
|
// OPT_CONTROL,
|
||||||
|
// OPT_CONTROL_PATH,
|
||||||
|
|
||||||
OPT_PARAMETER,
|
OPT_PARAMETER,
|
||||||
OPT_PARAMETER_NAME,
|
OPT_PARAMETER_NAME,
|
||||||
OPT_PARAMETER_VALUE,
|
OPT_PARAMETER_VALUE,
|
||||||
|
|
||||||
|
OPT_SCRIPT,
|
||||||
|
OPT_SCRIPT_NAME,
|
||||||
|
OPT_SCRIPT_PATH,
|
||||||
|
|
||||||
ARG_SOURCE_DIR,
|
ARG_SOURCE_DIR,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static int add_file_to_archive_ex(
|
||||||
|
struct ropkg_writer *writer,
|
||||||
|
b_directory *src_dir,
|
||||||
|
const b_path *src,
|
||||||
|
const char *dest)
|
||||||
|
{
|
||||||
|
b_file *fp;
|
||||||
|
b_status status = b_file_open(
|
||||||
|
src_dir,
|
||||||
|
src,
|
||||||
|
B_FILE_READ_ONLY | B_FILE_BINARY,
|
||||||
|
&fp);
|
||||||
|
if (!fp) {
|
||||||
|
b_err("cannot open file '%s'", src);
|
||||||
|
b_i("reason: %s", b_status_to_string(status));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ropkg_writer_file_info file = {0};
|
||||||
|
|
||||||
|
b_file_size(fp, &file.f_length);
|
||||||
|
|
||||||
|
ropkg_writer_begin_file(writer, dest, &file);
|
||||||
|
|
||||||
|
char buf[4096];
|
||||||
|
size_t offset = 0;
|
||||||
|
int c;
|
||||||
|
while (1) {
|
||||||
|
size_t r;
|
||||||
|
status = b_file_read(fp, offset, sizeof buf, buf, &r);
|
||||||
|
if (!B_OK(status)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t w;
|
||||||
|
char s = c;
|
||||||
|
ropkg_writer_write(writer, buf, r, &w);
|
||||||
|
offset += r;
|
||||||
|
|
||||||
|
if (r < sizeof buf) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b_file_release(fp);
|
||||||
|
ropkg_writer_end_file(writer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int add_file_to_archive(
|
||||||
|
struct ropkg_writer *writer,
|
||||||
|
const char *src,
|
||||||
|
const char *dest)
|
||||||
|
{
|
||||||
|
b_path *src_path = b_path_create_from_cstr(src);
|
||||||
|
int ret = add_file_to_archive_ex(writer, NULL, src_path, dest);
|
||||||
|
b_path_release(src_path);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int add_data_archive(
|
||||||
|
const b_arglist *args,
|
||||||
|
struct ropkg_stream *stream,
|
||||||
|
struct ropkg_writer *pkg)
|
||||||
|
{
|
||||||
|
const char *data_path_cstr;
|
||||||
|
b_status bstatus = b_arglist_get_string(
|
||||||
|
args,
|
||||||
|
B_COMMAND_INVALID_ID,
|
||||||
|
ARG_SOURCE_DIR,
|
||||||
|
0,
|
||||||
|
&data_path_cstr);
|
||||||
|
if (!B_OK(bstatus)) {
|
||||||
|
b_arglist_report_missing_option(args, OPT_MANIFEST);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
b_path *data_path = b_path_create_from_cstr(data_path_cstr);
|
||||||
|
b_directory *data_dir;
|
||||||
|
bstatus = b_directory_open(NULL, data_path, &data_dir);
|
||||||
|
if (!B_OK(bstatus)) {
|
||||||
|
b_err("cannot open directory '%s'", data_path_cstr);
|
||||||
|
b_i("reason: %s", b_status_to_string(bstatus));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ropkg_writer *data;
|
||||||
|
struct ropkg_writer_file_info file = {0};
|
||||||
|
ropkg_writer_begin_file(pkg, ROPKG_PATH_DATA ".zst", &file);
|
||||||
|
|
||||||
|
ropkg_stream_begin_compressed_section(stream);
|
||||||
|
enum ropkg_status status = ropkg_writer_open(stream, &data);
|
||||||
|
|
||||||
|
int ret = 0;
|
||||||
|
b_directory_iterator it;
|
||||||
|
b_directory_foreach(&it, data_dir, B_DIRECTORY_ITERATE_PARENT_FIRST)
|
||||||
|
{
|
||||||
|
if (!(it.info.attrib & B_FILE_ATTRIB_NORMAL)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = add_file_to_archive_ex(
|
||||||
|
data,
|
||||||
|
data_dir,
|
||||||
|
it.filepath,
|
||||||
|
b_path_ptr(it.filepath));
|
||||||
|
|
||||||
|
if (ret != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ropkg_writer_close(data);
|
||||||
|
ropkg_stream_end_compressed_section(stream, NULL, NULL);
|
||||||
|
ropkg_writer_end_file(pkg);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int add_control_archive(
|
||||||
|
const b_arglist *args,
|
||||||
|
struct ropkg_stream *stream,
|
||||||
|
struct ropkg_writer *pkg)
|
||||||
|
{
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
struct ropkg_writer *control;
|
||||||
|
struct ropkg_writer_file_info file = {0};
|
||||||
|
ropkg_writer_begin_file(pkg, ROPKG_PATH_CONTROL ".zst", &file);
|
||||||
|
|
||||||
|
ropkg_stream_begin_compressed_section(stream);
|
||||||
|
enum ropkg_status status = ropkg_writer_open(stream, &control);
|
||||||
|
|
||||||
|
b_arglist_option_iterator it;
|
||||||
|
b_arglist_option_foreach_filtered(&it, args, OPT_SCRIPT)
|
||||||
|
{
|
||||||
|
struct b_arglist_value *type, *path;
|
||||||
|
b_arglist_option_get_value(it.opt, OPT_SCRIPT_NAME, 0, &type);
|
||||||
|
b_arglist_option_get_value(it.opt, OPT_SCRIPT_PATH, 0, &path);
|
||||||
|
|
||||||
|
ret = add_file_to_archive(
|
||||||
|
control,
|
||||||
|
path->val_str,
|
||||||
|
type->val_str);
|
||||||
|
|
||||||
|
if (ret != 0) {
|
||||||
|
ropkg_writer_close(control);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ropkg_writer_close(control);
|
||||||
|
|
||||||
|
ropkg_stream_end_compressed_section(stream, NULL, NULL);
|
||||||
|
|
||||||
|
ropkg_writer_end_file(pkg);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int add_meta_archive(
|
||||||
|
const b_arglist *args,
|
||||||
|
struct ropkg_stream *stream,
|
||||||
|
struct ropkg_writer *pkg)
|
||||||
|
{
|
||||||
|
const char *manifest_path, *news_path;
|
||||||
|
b_status bstatus = b_arglist_get_string(
|
||||||
|
args,
|
||||||
|
OPT_MANIFEST,
|
||||||
|
OPT_MANIFEST_PATH,
|
||||||
|
1,
|
||||||
|
&manifest_path);
|
||||||
|
if (!B_OK(bstatus)) {
|
||||||
|
b_arglist_report_missing_option(args, OPT_MANIFEST);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
struct ropkg_writer *meta;
|
||||||
|
struct ropkg_writer_file_info file = {0};
|
||||||
|
ropkg_writer_begin_file(pkg, ROPKG_PATH_META ".zst", &file);
|
||||||
|
|
||||||
|
ropkg_stream_begin_compressed_section(stream);
|
||||||
|
enum ropkg_status status = ropkg_writer_open(stream, &meta);
|
||||||
|
|
||||||
|
bstatus = b_arglist_get_string(
|
||||||
|
args,
|
||||||
|
OPT_NEWS,
|
||||||
|
OPT_NEWS_PATH,
|
||||||
|
1,
|
||||||
|
&news_path);
|
||||||
|
if (B_OK(bstatus)) {
|
||||||
|
ret = add_file_to_archive(meta, news_path, "news");
|
||||||
|
if (ret != 0) {
|
||||||
|
ropkg_writer_close(meta);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = add_file_to_archive(meta, manifest_path, "manifest");
|
||||||
|
if (ret != 0) {
|
||||||
|
ropkg_writer_close(meta);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
ropkg_writer_close(meta);
|
||||||
|
|
||||||
|
ropkg_stream_end_compressed_section(stream, NULL, NULL);
|
||||||
|
|
||||||
|
ropkg_writer_end_file(pkg);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int create(
|
static int create(
|
||||||
const b_command *self,
|
const b_command *self,
|
||||||
const b_arglist *opt,
|
const b_arglist *opt,
|
||||||
const b_array *args)
|
const b_array *args)
|
||||||
{
|
{
|
||||||
return 0;
|
const struct ropkg_compression_function *zstd
|
||||||
|
= ropkg_compression_function_for_type(ROPKG_COMPRESSION_ZSTD);
|
||||||
|
struct ropkg_writer *pkg, *subpkg;
|
||||||
|
enum ropkg_status status;
|
||||||
|
|
||||||
|
const char *out_path;
|
||||||
|
b_status bstatus = b_arglist_get_string(
|
||||||
|
opt,
|
||||||
|
OPT_OUTPATH,
|
||||||
|
OPT_OUTPATH_PATH,
|
||||||
|
1,
|
||||||
|
&out_path);
|
||||||
|
if (!B_OK(bstatus)) {
|
||||||
|
b_arglist_report_missing_option(opt, OPT_OUTPATH);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *fp = fopen(out_path, "wb");
|
||||||
|
struct ropkg_stream *stream;
|
||||||
|
status = ropkg_stream_open(fp, zstd, ROPKG_STREAM_WRITE, &stream);
|
||||||
|
|
||||||
|
status = ropkg_writer_open(stream, &pkg);
|
||||||
|
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
ret = add_data_archive(opt, stream, pkg);
|
||||||
|
if (ret != 0) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = add_control_archive(opt, stream, pkg);
|
||||||
|
if (ret != 0) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = add_meta_archive(opt, stream, pkg);
|
||||||
|
if (ret != 0) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
end:
|
||||||
|
ropkg_writer_close(pkg);
|
||||||
|
ropkg_stream_close(stream);
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
B_COMMAND(CMD_CREATE, CMD_ROOT)
|
B_COMMAND(CMD_CREATE, CMD_ROOT)
|
||||||
@@ -53,10 +332,7 @@ B_COMMAND(CMD_CREATE, CMD_ROOT)
|
|||||||
B_OPTION_LONG_NAME("manifest");
|
B_OPTION_LONG_NAME("manifest");
|
||||||
B_OPTION_DESC(
|
B_OPTION_DESC(
|
||||||
"the path to a manifest file describing the "
|
"the path to a manifest file describing the "
|
||||||
"package to be created. if no manifest is specified, "
|
"package to be created.");
|
||||||
"the package binary directory will be searched for a "
|
|
||||||
"manifest");
|
|
||||||
|
|
||||||
B_OPTION_ARG(OPT_MANIFEST_PATH)
|
B_OPTION_ARG(OPT_MANIFEST_PATH)
|
||||||
{
|
{
|
||||||
B_ARG_NAME("path");
|
B_ARG_NAME("path");
|
||||||
@@ -64,6 +340,63 @@ B_COMMAND(CMD_CREATE, CMD_ROOT)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
B_COMMAND_OPTION(OPT_NEWS)
|
||||||
|
{
|
||||||
|
B_OPTION_SHORT_NAME('n');
|
||||||
|
B_OPTION_LONG_NAME("news");
|
||||||
|
B_OPTION_DESC(
|
||||||
|
"the path to a news file to be included in the "
|
||||||
|
"package.");
|
||||||
|
B_OPTION_ARG(OPT_NEWS_PATH)
|
||||||
|
{
|
||||||
|
B_ARG_NAME("path");
|
||||||
|
B_ARG_NR_VALUES(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
B_COMMAND_OPTION(OPT_CONTROL)
|
||||||
|
{
|
||||||
|
B_OPTION_SHORT_NAME('c');
|
||||||
|
B_OPTION_LONG_NAME("control");
|
||||||
|
B_OPTION_DESC(
|
||||||
|
"the path to a control directory containing package "
|
||||||
|
"scripts. this option cannot be used with "
|
||||||
|
"-s/--script.");
|
||||||
|
B_OPTION_ARG(OPT_CONTROL_PATH)
|
||||||
|
{
|
||||||
|
B_ARG_NAME("path");
|
||||||
|
B_ARG_NR_VALUES(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
B_COMMAND_OPTION(OPT_SCRIPT)
|
||||||
|
{
|
||||||
|
B_OPTION_SHORT_NAME('s');
|
||||||
|
B_OPTION_LONG_NAME("script");
|
||||||
|
B_OPTION_DESC(
|
||||||
|
"a control script to be run at a certain point in the "
|
||||||
|
"package (un)installation process.");
|
||||||
|
|
||||||
|
B_OPTION_ARG(OPT_SCRIPT_NAME)
|
||||||
|
{
|
||||||
|
B_ARG_NAME("event");
|
||||||
|
B_ARG_NR_VALUES(1);
|
||||||
|
B_ARG_ALLOWED_VALUES(
|
||||||
|
"pre-install",
|
||||||
|
"post-install",
|
||||||
|
"pre-uninstall",
|
||||||
|
"post-uninstall");
|
||||||
|
}
|
||||||
|
|
||||||
|
B_OPTION_ARG(OPT_SCRIPT_PATH)
|
||||||
|
{
|
||||||
|
B_ARG_NAME("path");
|
||||||
|
B_ARG_NR_VALUES(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
B_COMMAND_OPTION(OPT_PARAMETER)
|
B_COMMAND_OPTION(OPT_PARAMETER)
|
||||||
{
|
{
|
||||||
B_OPTION_SHORT_NAME('p');
|
B_OPTION_SHORT_NAME('p');
|
||||||
@@ -101,126 +434,7 @@ B_COMMAND(CMD_CREATE, CMD_ROOT)
|
|||||||
B_COMMAND_USAGE()
|
B_COMMAND_USAGE()
|
||||||
{
|
{
|
||||||
B_COMMAND_USAGE_OPT(OPT_OUTPATH);
|
B_COMMAND_USAGE_OPT(OPT_OUTPATH);
|
||||||
B_COMMAND_USAGE_ARG(ARG_SOURCE_DIR);
|
B_COMMAND_USAGE_OPT_PLACEHOLDER();
|
||||||
}
|
|
||||||
|
|
||||||
B_COMMAND_USAGE()
|
|
||||||
{
|
|
||||||
B_COMMAND_USAGE_OPT(OPT_MANIFEST);
|
|
||||||
B_COMMAND_USAGE_OPT(OPT_OUTPATH);
|
|
||||||
B_COMMAND_USAGE_ARG(ARG_SOURCE_DIR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#include "commands.h"
|
|
||||||
|
|
||||||
#include <blue/cmd.h>
|
|
||||||
|
|
||||||
enum {
|
|
||||||
OPT_OUTPATH,
|
|
||||||
OPT_OUTPATH_PATH,
|
|
||||||
|
|
||||||
OPT_MANIFEST,
|
|
||||||
OPT_MANIFEST_PATH,
|
|
||||||
|
|
||||||
OPT_PARAMETER,
|
|
||||||
OPT_PARAMETER_NAME,
|
|
||||||
OPT_PARAMETER_VALUE,
|
|
||||||
|
|
||||||
ARG_SOURCE_DIR,
|
|
||||||
};
|
|
||||||
|
|
||||||
static int create(
|
|
||||||
const b_command *self,
|
|
||||||
const b_arglist *opt,
|
|
||||||
const b_array *args)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
B_COMMAND(CMD_CREATE, CMD_ROOT)
|
|
||||||
{
|
|
||||||
B_COMMAND_NAME("create");
|
|
||||||
B_COMMAND_SHORT_NAME('C');
|
|
||||||
B_COMMAND_DESC("create a Rosetta package from a directory");
|
|
||||||
B_COMMAND_FLAGS(B_COMMAND_SHOW_HELP_BY_DEFAULT);
|
|
||||||
B_COMMAND_FUNCTION(create);
|
|
||||||
|
|
||||||
B_COMMAND_HELP_OPTION();
|
|
||||||
|
|
||||||
B_COMMAND_OPTION(OPT_OUTPATH)
|
|
||||||
{
|
|
||||||
B_OPTION_SHORT_NAME('o');
|
|
||||||
B_OPTION_LONG_NAME("out");
|
|
||||||
B_OPTION_DESC("the path to save the new package file to");
|
|
||||||
|
|
||||||
B_OPTION_ARG(OPT_OUTPATH_PATH)
|
|
||||||
{
|
|
||||||
B_ARG_NAME("path");
|
|
||||||
B_ARG_NR_VALUES(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
B_COMMAND_OPTION(OPT_MANIFEST)
|
|
||||||
{
|
|
||||||
B_OPTION_SHORT_NAME('m');
|
|
||||||
B_OPTION_LONG_NAME("manifest");
|
|
||||||
B_OPTION_DESC(
|
|
||||||
"the path to a manifest file describing the "
|
|
||||||
"package to be created. if no manifest is specified, "
|
|
||||||
"the package binary directory will be searched for a "
|
|
||||||
"manifest");
|
|
||||||
|
|
||||||
B_OPTION_ARG(OPT_MANIFEST_PATH)
|
|
||||||
{
|
|
||||||
B_ARG_NAME("path");
|
|
||||||
B_ARG_NR_VALUES(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
B_COMMAND_OPTION(OPT_PARAMETER)
|
|
||||||
{
|
|
||||||
B_OPTION_SHORT_NAME('p');
|
|
||||||
B_OPTION_LONG_NAME("parameter");
|
|
||||||
B_OPTION_DESC(
|
|
||||||
"a parameter to use when creating the new file. "
|
|
||||||
"if a parameter EXAMPLE is defined, any variable "
|
|
||||||
"reference "
|
|
||||||
"of the form ${EXAMPLE} in the package manifest will "
|
|
||||||
"be "
|
|
||||||
"replaced with the specified parameter value.");
|
|
||||||
|
|
||||||
B_OPTION_ARG(OPT_PARAMETER_NAME)
|
|
||||||
{
|
|
||||||
B_ARG_NAME("name");
|
|
||||||
B_ARG_NR_VALUES(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
B_OPTION_ARG(OPT_PARAMETER_VALUE)
|
|
||||||
{
|
|
||||||
B_ARG_NAME("value");
|
|
||||||
B_ARG_NR_VALUES(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
B_COMMAND_ARG(ARG_SOURCE_DIR)
|
|
||||||
{
|
|
||||||
B_ARG_NAME("source-dir");
|
|
||||||
B_ARG_DESC(
|
|
||||||
"the directory to package. the specified directory "
|
|
||||||
"will become the root of the newly created package.");
|
|
||||||
B_ARG_NR_VALUES(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
B_COMMAND_USAGE()
|
|
||||||
{
|
|
||||||
B_COMMAND_USAGE_OPT(OPT_OUTPATH);
|
|
||||||
B_COMMAND_USAGE_ARG(ARG_SOURCE_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
B_COMMAND_USAGE()
|
|
||||||
{
|
|
||||||
B_COMMAND_USAGE_OPT(OPT_MANIFEST);
|
|
||||||
B_COMMAND_USAGE_OPT(OPT_OUTPATH);
|
|
||||||
B_COMMAND_USAGE_ARG(ARG_SOURCE_DIR);
|
B_COMMAND_USAGE_ARG(ARG_SOURCE_DIR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user