Add support for appendable multisession images.
This commit is contained in:
parent
0fdfe05d7e
commit
ee78419935
@ -33,3 +33,4 @@ demo/catbuffer
|
|||||||
demo/isoread
|
demo/isoread
|
||||||
demo/isocat
|
demo/isocat
|
||||||
demo/isomodify
|
demo/isomodify
|
||||||
|
demo/isoms
|
||||||
|
@ -60,7 +60,8 @@ noinst_PROGRAMS = \
|
|||||||
demo/iso \
|
demo/iso \
|
||||||
demo/isoread \
|
demo/isoread \
|
||||||
demo/isocat \
|
demo/isocat \
|
||||||
demo/isomodify
|
demo/isomodify \
|
||||||
|
demo/isoms
|
||||||
|
|
||||||
demo_lsl_CPPFLAGS = -Isrc
|
demo_lsl_CPPFLAGS = -Isrc
|
||||||
demo_lsl_LDADD = $(src_libisofs_la_OBJECTS) $(THREAD_LIBS)
|
demo_lsl_LDADD = $(src_libisofs_la_OBJECTS) $(THREAD_LIBS)
|
||||||
@ -98,6 +99,10 @@ demo_isomodify_CPPFLAGS = -Isrc
|
|||||||
demo_isomodify_LDADD = $(src_libisofs_la_OBJECTS) $(THREAD_LIBS)
|
demo_isomodify_LDADD = $(src_libisofs_la_OBJECTS) $(THREAD_LIBS)
|
||||||
demo_isomodify_SOURCES = demo/iso_modify.c
|
demo_isomodify_SOURCES = demo/iso_modify.c
|
||||||
|
|
||||||
|
demo_isoms_CPPFLAGS = -Isrc
|
||||||
|
demo_isoms_LDADD = $(src_libisofs_la_OBJECTS) $(THREAD_LIBS)
|
||||||
|
demo_isoms_SOURCES = demo/iso_ms.c
|
||||||
|
|
||||||
|
|
||||||
## Build unit test
|
## Build unit test
|
||||||
|
|
||||||
|
@ -41,7 +41,10 @@ int main(int argc, char **argv)
|
|||||||
0, /* file_mode */
|
0, /* file_mode */
|
||||||
0, /* uid */
|
0, /* uid */
|
||||||
0, /* gid */
|
0, /* gid */
|
||||||
NULL /* output charset */
|
NULL, /* output charset */
|
||||||
|
0, /* appendable */
|
||||||
|
0, /* ms_block */
|
||||||
|
NULL /* overwrite */
|
||||||
};
|
};
|
||||||
|
|
||||||
if (argc < 2) {
|
if (argc < 2) {
|
||||||
|
@ -41,7 +41,10 @@ int main(int argc, char **argv)
|
|||||||
0, /* file_mode */
|
0, /* file_mode */
|
||||||
0, /* uid */
|
0, /* uid */
|
||||||
0, /* gid */
|
0, /* gid */
|
||||||
NULL /* output charset */
|
NULL, /* output charset */
|
||||||
|
0, /* appendable */
|
||||||
|
0, /* ms_block */
|
||||||
|
NULL /* overwrite */
|
||||||
};
|
};
|
||||||
struct iso_read_opts ropts = {
|
struct iso_read_opts ropts = {
|
||||||
0, /* block */
|
0, /* block */
|
||||||
|
122
demo/iso_ms.c
Normal file
122
demo/iso_ms.c
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* Little program to show how to create a multisession iso image.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "libisofs.h"
|
||||||
|
#include "libburn/libburn.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <err.h>
|
||||||
|
|
||||||
|
void usage(char **argv)
|
||||||
|
{
|
||||||
|
printf("%s LSS NWA DISC DIRECTORY OUTPUT\n", argv[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int result;
|
||||||
|
IsoImage *image;
|
||||||
|
IsoDataSource *src;
|
||||||
|
struct burn_source *burn_src;
|
||||||
|
unsigned char buf[2048];
|
||||||
|
FILE *fd;
|
||||||
|
Ecma119WriteOpts opts = {
|
||||||
|
1, /* level */
|
||||||
|
1, /* rockridge */
|
||||||
|
0, /* omit_version_numbers */
|
||||||
|
0, /* allow_deep_paths */
|
||||||
|
1, /* sort files */
|
||||||
|
0, /* replace_dir_mode */
|
||||||
|
0, /* replace_file_mode */
|
||||||
|
0, /* replace_uid */
|
||||||
|
0, /* replace_gid */
|
||||||
|
0, /* dir_mode */
|
||||||
|
0, /* file_mode */
|
||||||
|
0, /* uid */
|
||||||
|
0, /* gid */
|
||||||
|
NULL, /* output charset */
|
||||||
|
0, /* appendable */
|
||||||
|
0, /* ms_block */
|
||||||
|
NULL /* overwrite */
|
||||||
|
};
|
||||||
|
struct iso_read_opts ropts = {
|
||||||
|
0, /* block */
|
||||||
|
0, /* norock */
|
||||||
|
0, /* nojoliet */
|
||||||
|
0, /* preferjoliet */
|
||||||
|
0, /* uid; */
|
||||||
|
0, /* gid; */
|
||||||
|
0, /* mode */
|
||||||
|
"UTF-8" /* input_charset */
|
||||||
|
};
|
||||||
|
|
||||||
|
if (argc < 6) {
|
||||||
|
usage(argv);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fd = fopen(argv[5], "w");
|
||||||
|
if (!fd) {
|
||||||
|
err(1, "error opening output file");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* create the data source to accesss previous image */
|
||||||
|
result = iso_data_source_new_from_file(argv[3], &src);
|
||||||
|
if (result < 0) {
|
||||||
|
printf ("Error creating data source\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* create the image context */
|
||||||
|
result = iso_image_new("volume_id", &image);
|
||||||
|
if (result < 0) {
|
||||||
|
printf ("Error creating image\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
iso_image_set_msgs_severities(image, "NEVER", "ALL", "");
|
||||||
|
iso_tree_set_follow_symlinks(image, 0);
|
||||||
|
iso_tree_set_ignore_hidden(image, 0);
|
||||||
|
iso_tree_set_stop_on_error(image, 0);
|
||||||
|
|
||||||
|
/* import previous image */
|
||||||
|
ropts.block = atoi(argv[1]);
|
||||||
|
result = iso_image_import(image, src, &ropts, NULL);
|
||||||
|
iso_data_source_unref(src);
|
||||||
|
if (result < 0) {
|
||||||
|
printf ("Error importing previous session %d\n", result);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* add new dir */
|
||||||
|
result = iso_tree_add_dir_rec(image, iso_image_get_root(image), argv[4]);
|
||||||
|
if (result < 0) {
|
||||||
|
printf ("Error adding directory %d\n", result);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* generate a multisession image with new contents */
|
||||||
|
opts.ms_block = atoi(argv[2]);
|
||||||
|
opts.appendable = 1;
|
||||||
|
result = iso_image_create(image, &opts, &burn_src);
|
||||||
|
if (result < 0) {
|
||||||
|
printf ("Cant create image, error %d\n", result);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (burn_src->read(burn_src, buf, 2048) == 2048) {
|
||||||
|
fwrite(buf, 1, 2048, fd);
|
||||||
|
}
|
||||||
|
fclose(fd);
|
||||||
|
burn_src->free_data(burn_src);
|
||||||
|
free(burn_src);
|
||||||
|
|
||||||
|
iso_image_unref(image);
|
||||||
|
return 0;
|
||||||
|
}
|
@ -754,7 +754,8 @@ int ecma119_image_new(IsoImage *src, Ecma119WriteOpts *opts, Ecma119Image **img)
|
|||||||
target->file_mode = opts->replace_file_mode == 2 ? opts->file_mode : 0444;
|
target->file_mode = opts->replace_file_mode == 2 ? opts->file_mode : 0444;
|
||||||
|
|
||||||
target->now = time(NULL);
|
target->now = time(NULL);
|
||||||
target->ms_block = 0;
|
target->ms_block = opts->ms_block;
|
||||||
|
target->appendable = opts->appendable;
|
||||||
|
|
||||||
/* default to locale charset */
|
/* default to locale charset */
|
||||||
setlocale(LC_CTYPE, "");
|
setlocale(LC_CTYPE, "");
|
||||||
|
@ -66,6 +66,7 @@ struct ecma119_image
|
|||||||
char *input_charset;
|
char *input_charset;
|
||||||
char *output_charset;
|
char *output_charset;
|
||||||
|
|
||||||
|
unsigned int appendable : 1;
|
||||||
uint32_t ms_block; /**< start block for a ms image */
|
uint32_t ms_block; /**< start block for a ms image */
|
||||||
time_t now; /**< Time at which writing began. */
|
time_t now; /**< Time at which writing began. */
|
||||||
|
|
||||||
|
@ -113,12 +113,20 @@ static int cmp_by_weight(const void *f1, const void *f2)
|
|||||||
return g->sort_weight - f->sort_weight;
|
return g->sort_weight - f->sort_weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
int is_ms_file(void *arg)
|
||||||
|
{
|
||||||
|
IsoFileSrc *f = (IsoFileSrc *)arg;
|
||||||
|
return f->prev_img ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
int filesrc_writer_compute_data_blocks(IsoImageWriter *writer)
|
int filesrc_writer_compute_data_blocks(IsoImageWriter *writer)
|
||||||
{
|
{
|
||||||
size_t i, size;
|
size_t i, size;
|
||||||
Ecma119Image *t;
|
Ecma119Image *t;
|
||||||
IsoFileSrc **filelist;
|
IsoFileSrc **filelist;
|
||||||
|
int (*inc_item)(void *);
|
||||||
|
|
||||||
if (writer == NULL) {
|
if (writer == NULL) {
|
||||||
return ISO_MEM_ERROR;
|
return ISO_MEM_ERROR;
|
||||||
@ -126,14 +134,19 @@ int filesrc_writer_compute_data_blocks(IsoImageWriter *writer)
|
|||||||
|
|
||||||
t = writer->target;
|
t = writer->target;
|
||||||
|
|
||||||
|
/* on appendable images, ms files shouldn't be included */
|
||||||
|
if (t->appendable) {
|
||||||
|
inc_item = is_ms_file;
|
||||||
|
} else {
|
||||||
|
inc_item = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* store the filesrcs in a array */
|
/* store the filesrcs in a array */
|
||||||
filelist = (IsoFileSrc**)iso_rbtree_to_array(t->files);
|
filelist = (IsoFileSrc**)iso_rbtree_to_array(t->files, inc_item, &size);
|
||||||
if (filelist == NULL) {
|
if (filelist == NULL) {
|
||||||
return ISO_MEM_ERROR;
|
return ISO_MEM_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
size = iso_rbtree_get_size(t->files);
|
|
||||||
|
|
||||||
/* sort files by weight, if needed */
|
/* sort files by weight, if needed */
|
||||||
if (t->sort_files) {
|
if (t->sort_files) {
|
||||||
qsort(filelist, size, sizeof(void*), cmp_by_weight);
|
qsort(filelist, size, sizeof(void*), cmp_by_weight);
|
||||||
@ -207,8 +220,9 @@ static
|
|||||||
int filesrc_writer_write_data(IsoImageWriter *writer)
|
int filesrc_writer_write_data(IsoImageWriter *writer)
|
||||||
{
|
{
|
||||||
int res;
|
int res;
|
||||||
size_t i, b, nfiles;
|
size_t i, b;
|
||||||
Ecma119Image *t;
|
Ecma119Image *t;
|
||||||
|
IsoFileSrc *file;
|
||||||
IsoFileSrc **filelist;
|
IsoFileSrc **filelist;
|
||||||
char buffer[BLOCK_SIZE];
|
char buffer[BLOCK_SIZE];
|
||||||
|
|
||||||
@ -221,9 +235,8 @@ int filesrc_writer_write_data(IsoImageWriter *writer)
|
|||||||
|
|
||||||
iso_msg_debug(t->image->messenger, "Writing Files...");
|
iso_msg_debug(t->image->messenger, "Writing Files...");
|
||||||
|
|
||||||
nfiles = iso_rbtree_get_size(t->files);
|
i = 0;
|
||||||
for (i = 0; i < nfiles; ++i) {
|
while ((file = filelist[i++]) != NULL) {
|
||||||
IsoFileSrc *file = filelist[i];
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TODO WARNING
|
* TODO WARNING
|
||||||
|
@ -1475,7 +1475,7 @@ int iso_image_filesystem_new(IsoDataSource *src, struct iso_read_opts *opts,
|
|||||||
/* get our ref to IsoDataSource */
|
/* get our ref to IsoDataSource */
|
||||||
data->src = src;
|
data->src = src;
|
||||||
iso_data_source_ref(src);
|
iso_data_source_ref(src);
|
||||||
data->open_count = 0; //TODO
|
data->open_count = 0;
|
||||||
|
|
||||||
/* get an id for the filesystem */
|
/* get an id for the filesystem */
|
||||||
data->id = ++fs_dev_id;
|
data->id = ++fs_dev_id;
|
||||||
|
@ -104,36 +104,69 @@ typedef struct
|
|||||||
uid_t uid; /** uid to use when replace_uid == 2. */
|
uid_t uid; /** uid to use when replace_uid == 2. */
|
||||||
gid_t gid; /** gid to use when replace_gid == 2. */
|
gid_t gid; /** gid to use when replace_gid == 2. */
|
||||||
|
|
||||||
char *output_charset; /**< NULL to use default charset */
|
/**
|
||||||
// uint32_t ms_block;
|
* Charset for the RR filenames that will be created.
|
||||||
/**<
|
* NULL to use default charset, the locale one.
|
||||||
* Start block for multisession. When this is greater than 0,
|
|
||||||
* it's suppossed to be the lba of the next writable address
|
|
||||||
* on disc; all block lba on image will take this into account,
|
|
||||||
* and files from a previous session will not be written on
|
|
||||||
* image. This behavior is only suitable for images to be
|
|
||||||
* appended to a multisession disc.
|
|
||||||
* When this is 0, no multisession image will be created. If
|
|
||||||
* some files are taken from a previous image, its contents
|
|
||||||
* will be written again to the new image. Use this with new
|
|
||||||
* images or if you plan to modify an existin image.
|
|
||||||
*/
|
*/
|
||||||
// struct data_source* src;
|
char *output_charset;
|
||||||
// /**<
|
|
||||||
// * When modifying a image, this is the source of the original
|
/**
|
||||||
// * image, used to read file contents.
|
* This flags control the type of the image to create. Libisofs support
|
||||||
// * Otherwise it can be NULL.
|
* two kind of images: stand-alone and appendable.
|
||||||
// */
|
*
|
||||||
// uint8_t *overwrite;
|
* A stand-alone image is an image that is valid alone, and that can be
|
||||||
// /**<
|
* mounted by its own. This is the kind of image you will want to create
|
||||||
// * When not NULL, it should point to a buffer of at least
|
* in most cases. A stand-alone image can be burned in an empty CD or DVD,
|
||||||
// * 64KiB, where libisofs will write the contents that should
|
* or write to an .iso file for future burning or distribution.
|
||||||
// * be written at the beginning of a overwriteable media, to
|
*
|
||||||
// * grow the image.
|
* On the other side, an appendable image is not self contained, it refers
|
||||||
// * You shoudl initialize the buffer either with 0s, or with
|
* to serveral files that are stored outside the image. Its usage is for
|
||||||
// * the contents of the first blocks of the image you're
|
* multisession discs, where you add data in a new session, while the
|
||||||
// * growing. In most cases, 0 is good enought.
|
* previous session data can still be accessed. In those cases, the old
|
||||||
// */
|
* data is not written again. Instead, the new image refers to it, and thus
|
||||||
|
* it's only valid when appended to the original. Note that in those cases
|
||||||
|
* the image will be written after the original, and thus you will want
|
||||||
|
* to use a ms_block greater than 0.
|
||||||
|
*
|
||||||
|
* Note that if you haven't import a previous image (by means of
|
||||||
|
* iso_image_import()), the image will always be a stand-alone image, as
|
||||||
|
* there is no previous data to refer to.
|
||||||
|
*/
|
||||||
|
unsigned int appendable : 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start block of the image. It is supposed to be the lba where the first
|
||||||
|
* block of the image will be written on disc. All references inside the
|
||||||
|
* ISO image will take this into account, thus providing a mountable image.
|
||||||
|
*
|
||||||
|
* For appendable images, that are written to a new session, you should
|
||||||
|
* pass here the lba of the next writable address on disc.
|
||||||
|
*
|
||||||
|
* In stand alone images this is usually 0. However, you may want to
|
||||||
|
* provide a different ms_block if you don't plan to burn the image in the
|
||||||
|
* first session on disc, such as in some CD-Extra disc whether the data
|
||||||
|
* image is written in a new session after some audio tracks.
|
||||||
|
*/
|
||||||
|
uint32_t ms_block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When not NULL, it should point to a buffer of at least 64KiB, where
|
||||||
|
* libisofs will write the contents that should be written at the beginning
|
||||||
|
* of a overwriteable media, to grow the image. The growing of an image is
|
||||||
|
* a way, used by first time in growisofs by Andy Polyakov, to allow the
|
||||||
|
* appending of new data to non-multisession media, such as DVD+RW, in the
|
||||||
|
* same way you append a new session to a multisession disc, i.e., without
|
||||||
|
* need to write again the contents of the previous image.
|
||||||
|
*
|
||||||
|
* Note that if you want this kind of image growing, you will also need to
|
||||||
|
* set appendable to "1" and provide a valid ms_block after the previous
|
||||||
|
* image.
|
||||||
|
*
|
||||||
|
* You should initialize the buffer either with 0s, or with the contents of
|
||||||
|
* the first blocks of the image you're growing. In most cases, 0 is good
|
||||||
|
* enought.
|
||||||
|
*/
|
||||||
|
uint8_t *overwrite;
|
||||||
} Ecma119WriteOpts;
|
} Ecma119WriteOpts;
|
||||||
|
|
||||||
typedef struct Iso_Data_Source IsoDataSource;
|
typedef struct Iso_Data_Source IsoDataSource;
|
||||||
|
11
src/util.h
11
src/util.h
@ -193,12 +193,21 @@ size_t iso_rbtree_get_size(IsoRBTree *tree);
|
|||||||
/**
|
/**
|
||||||
* Get an array view of the elements of the tree.
|
* Get an array view of the elements of the tree.
|
||||||
*
|
*
|
||||||
|
* @param include_item
|
||||||
|
* Function to select which elements to include in the array. It that takes
|
||||||
|
* a pointer to an element and returns 1 if the element should be included,
|
||||||
|
* 0 if not. If you want to add all elements to the array, you can pass a
|
||||||
|
* NULL pointer.
|
||||||
|
* @param size
|
||||||
|
* If not null, will be filled with the number of elements in the array,
|
||||||
|
* without counting the final NULL item.
|
||||||
* @return
|
* @return
|
||||||
* A sorted array with the contents of the tree, or NULL if there is not
|
* A sorted array with the contents of the tree, or NULL if there is not
|
||||||
* enought memory to allocate the array. You should free(3) the array when
|
* enought memory to allocate the array. You should free(3) the array when
|
||||||
* no more needed. Note that the array is NULL-terminated, and thus it
|
* no more needed. Note that the array is NULL-terminated, and thus it
|
||||||
* has size + 1 length.
|
* has size + 1 length.
|
||||||
*/
|
*/
|
||||||
void **iso_rbtree_to_array(IsoRBTree *tree);
|
void **iso_rbtree_to_array(IsoRBTree *tree, int (*include_item)(void *),
|
||||||
|
size_t *size);
|
||||||
|
|
||||||
#endif /*LIBISO_UTIL_H_*/
|
#endif /*LIBISO_UTIL_H_*/
|
||||||
|
@ -244,28 +244,38 @@ size_t iso_rbtree_get_size(IsoRBTree *tree)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
int rbtree_to_array_aux(struct iso_rbnode *root, void **array, size_t pos)
|
size_t rbtree_to_array_aux(struct iso_rbnode *root, void **array, size_t pos,
|
||||||
|
int (*include_item)(void *))
|
||||||
{
|
{
|
||||||
if (root == NULL) {
|
if (root == NULL) {
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
pos = rbtree_to_array_aux(root->ch[0], array, pos);
|
pos = rbtree_to_array_aux(root->ch[0], array, pos, include_item);
|
||||||
array[pos++] = root->data;
|
if (include_item == NULL || include_item(root->data)) {
|
||||||
pos = rbtree_to_array_aux(root->ch[1], array, pos);
|
array[pos++] = root->data;
|
||||||
|
}
|
||||||
|
pos = rbtree_to_array_aux(root->ch[1], array, pos, include_item);
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an array view of the elements of the tree.
|
* Get an array view of the elements of the tree.
|
||||||
*
|
*
|
||||||
|
* @param include_item
|
||||||
|
* Function to select which elements to include in the array. It that takes
|
||||||
|
* a pointer to an element and returns 1 if the element should be included,
|
||||||
|
* 0 if not. If you want to add all elements to the array, you can pass a
|
||||||
|
* NULL pointer.
|
||||||
* @return
|
* @return
|
||||||
* A sorted array with the contents of the tree, or NULL if there is not
|
* A sorted array with the contents of the tree, or NULL if there is not
|
||||||
* enought memory to allocate the array. You should free(3) the array when
|
* enought memory to allocate the array. You should free(3) the array when
|
||||||
* no more needed. Note that the array is NULL-terminated, and thus it
|
* no more needed. Note that the array is NULL-terminated, and thus it
|
||||||
* has size + 1 length.
|
* has size + 1 length.
|
||||||
*/
|
*/
|
||||||
void ** iso_rbtree_to_array(IsoRBTree *tree)
|
void ** iso_rbtree_to_array(IsoRBTree *tree, int (*include_item)(void *),
|
||||||
|
size_t *size)
|
||||||
{
|
{
|
||||||
|
size_t pos;
|
||||||
void **array;
|
void **array;
|
||||||
|
|
||||||
array = malloc((tree->size + 1) * sizeof(void*));
|
array = malloc((tree->size + 1) * sizeof(void*));
|
||||||
@ -274,9 +284,13 @@ void ** iso_rbtree_to_array(IsoRBTree *tree)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* fill array */
|
/* fill array */
|
||||||
rbtree_to_array_aux(tree->root, array, 0);
|
pos = rbtree_to_array_aux(tree->root, array, 0, include_item);
|
||||||
array[tree->size] = NULL;
|
array[pos] = NULL;
|
||||||
|
|
||||||
|
array = realloc(array, (pos + 1) * sizeof(void*));
|
||||||
|
if (size) {
|
||||||
|
*size = pos;
|
||||||
|
}
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user