mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-26 10:51:54 -04:00
Initial support for symlinks. Only in *nix.
This commit is contained in:
parent
4e7957ff73
commit
399f181a57
13 changed files with 679 additions and 139 deletions
5
HISTORY
5
HISTORY
|
|
@ -1,10 +1,11 @@
|
|||
SnapRAID HISTORY
|
||||
================
|
||||
|
||||
1.6 2011/8
|
||||
1.6 2011/9
|
||||
==========
|
||||
* In "diff" now recognizes the reuse of inodes.
|
||||
* In "diff" now it recognizes the reuse of inodes.
|
||||
* Windows hidden files are now backuped like any other files.
|
||||
* Symbolic links are now saved in *nix. Not supported in Windows.
|
||||
|
||||
1.5 2011/7
|
||||
==========
|
||||
|
|
|
|||
10
Makefile.am
10
Makefile.am
|
|
@ -92,13 +92,13 @@ check-local:
|
|||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -v -c $(srcdir)/test-raid6.conf sync
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -c $(srcdir)/test-raid6.conf check
|
||||
echo --- Create some files, sync raid6 and check
|
||||
$(TESTENV) ./mktest$(EXEEXT) generate 1 3 1000 4000
|
||||
$(TESTENV) ./mktest$(EXEEXT) generate 1 3 1500 4000
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -v -c $(srcdir)/test-raid6.conf diff
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -v -c $(srcdir)/test-raid6.conf sync
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -c $(srcdir)/test-raid5.conf check
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -c $(srcdir)/test-raid6.conf check
|
||||
echo --- Create some more files, sync raid5 and check
|
||||
$(TESTENV) ./mktest$(EXEEXT) generate 2 3 1000 4000
|
||||
$(TESTENV) ./mktest$(EXEEXT) generate 2 3 1500 4000
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -v -c $(srcdir)/test-raid5.conf diff
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -v -c $(srcdir)/test-raid5.conf sync
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -c $(srcdir)/test-raid5.conf check
|
||||
|
|
@ -114,7 +114,7 @@ check-local:
|
|||
rm test/disk1/1*
|
||||
rm test/disk2/2*
|
||||
rm test/disk3/3*
|
||||
$(TESTENV) ./mktest$(EXEEXT) generate 3 3 100 4000
|
||||
$(TESTENV) ./mktest$(EXEEXT) generate 3 3 200 4000
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -v -c $(srcdir)/test-raid6.conf sync
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -c $(srcdir)/test-raid6.conf check
|
||||
echo --- Move some files, sync and check
|
||||
|
|
@ -144,7 +144,7 @@ check-local:
|
|||
rm test/disk1/4*
|
||||
rm test/disk2/5*
|
||||
rm test/disk3/6*
|
||||
$(TESTENV) ./mktest$(EXEEXT) generate 4 3 100 4000
|
||||
$(TESTENV) ./mktest$(EXEEXT) generate 4 3 200 4000
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -v -c $(srcdir)/test-raid6.conf -t 100 sync
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -c $(srcdir)/test-raid6.conf check 2>> test.log
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -v -c $(srcdir)/test-raid6.conf -t 700 sync
|
||||
|
|
@ -154,7 +154,7 @@ check-local:
|
|||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -v -c $(srcdir)/test-raid6.conf sync
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -c $(srcdir)/test-raid6.conf check
|
||||
echo --- Delete some files, fix and check with raid5
|
||||
rm test/disk1/1*
|
||||
rm test/disk1/8*
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -R -c $(srcdir)/test-raid5.conf check 2>> test.log
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -c $(srcdir)/test-raid5.conf fix 2>> test.log
|
||||
$(TESTENV) ./snapraid$(EXEEXT) $(CHECKFLAGS) -S -c $(srcdir)/test-raid6.conf check
|
||||
|
|
|
|||
6
TODO
6
TODO
|
|
@ -3,11 +3,11 @@ SnapRAID TODO
|
|||
|
||||
This is a list of possible TODO items for SnapRAID. The order is not relevant.
|
||||
|
||||
* Support to backup also symlinks. It's important to allow a full backup as
|
||||
many software relay on them to store important information.
|
||||
|
||||
* When fixing, before overwriting the present file, make a copy of it just in
|
||||
case that the original file cannot be completely recovered.
|
||||
We can always open files in read-only mode, if a write is required, we close it,
|
||||
rename it to with a .bak extension, and rewrite it up to the required size.
|
||||
The same for symlink if a file with the same name exist or viceversa.
|
||||
|
||||
* If a file cannot be synched, for example because it was changing, or it
|
||||
the file name cannot be supported, it's better to do not stop the sync process.
|
||||
|
|
|
|||
89
check.c
89
check.c
|
|
@ -220,6 +220,7 @@ static int state_check_process(struct snapraid_state* state, int fix, int parity
|
|||
++countmax;
|
||||
}
|
||||
|
||||
/* check all the blocks in files */
|
||||
countsize = 0;
|
||||
countpos = 0;
|
||||
state_progress_begin(state, blockstart, blockmax, countmax);
|
||||
|
|
@ -518,6 +519,94 @@ static int state_check_process(struct snapraid_state* state, int fix, int parity
|
|||
}
|
||||
}
|
||||
|
||||
/* check all the links */
|
||||
for(i=0;i<diskmax;++i) {
|
||||
tommy_node* node;
|
||||
struct snapraid_disk* disk = tommy_array_get(&state->diskarr, i);
|
||||
|
||||
/* for each link in the disk */
|
||||
node = disk->linklist;
|
||||
while (node) {
|
||||
char path[PATH_MAX];
|
||||
char linkto[PATH_MAX];
|
||||
struct snapraid_link* link;
|
||||
int failed = 0;
|
||||
|
||||
link = node->data;
|
||||
node = node->next; /* next node */
|
||||
|
||||
/* if excluded continue to the next one */
|
||||
if (link_flag_has(link, FILE_IS_EXCLUDED)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* read the symlink */
|
||||
pathprint(path, sizeof(path), "%s%s", disk->dir, link->sub);
|
||||
ret = readlink(path, linkto, sizeof(linkto));
|
||||
if (ret < 0) {
|
||||
failed = 1;
|
||||
|
||||
fprintf(stderr, "Error reading symlink '%s'. %s.\n", path, strerror(errno));
|
||||
fprintf(stderr, "symlink_error:%s:%s: Symlink read error\n", disk->name, link->sub);
|
||||
++error;
|
||||
} else if (ret == sizeof(linkto)) {
|
||||
failed = 1;
|
||||
|
||||
fprintf(stderr, "Error reading symlink '%s'. Symlink too long.\n", path);
|
||||
fprintf(stderr, "symlink_error:%s:%s: Symlink read error\n", disk->name, link->sub);
|
||||
++error;
|
||||
} else {
|
||||
linkto[ret] = 0;
|
||||
|
||||
if (strcmp(linkto, link->linkto) != 0) {
|
||||
failed = 1;
|
||||
|
||||
fprintf(stderr, "symlink_error:%s:%s: Symlink data error '%s' instead of '%s'\n", disk->name, link->sub, linkto, link->linkto);
|
||||
++error;
|
||||
}
|
||||
}
|
||||
|
||||
if (fix && failed) {
|
||||
/* create the ancestor directories */
|
||||
ret = handle_ancestor(path);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "WARNING! Without a working data disk, it isn't possible to fix errors on it.\n");
|
||||
printf("Stopping\n");
|
||||
++unrecoverable_error;
|
||||
goto bail;
|
||||
}
|
||||
|
||||
/* if it exists, it must be deleted before recreating */
|
||||
ret = remove(path);
|
||||
if (ret != 0 && errno != ENOENT) {
|
||||
fprintf(stderr, "Error removing '%s'. %s.\n", path, strerror(errno));
|
||||
fprintf(stderr, "WARNING! Without a working data disk, it isn't possible to fix errors on it.\n");
|
||||
printf("Stopping\n");
|
||||
++unrecoverable_error;
|
||||
goto bail;
|
||||
}
|
||||
|
||||
/* create it */
|
||||
ret = symlink(link->linkto, path);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "Error writing symlink '%s'. %s.\n", path, strerror(errno));
|
||||
if (errno == EACCES) {
|
||||
fprintf(stderr, "WARNING! Please give write permission to the symlink.\n");
|
||||
} else {
|
||||
/* we do not use DANGER because it could be ENOSPC which is not always correctly reported */
|
||||
fprintf(stderr, "WARNING! Without a working data disk, it isn't possible to fix errors on it.\n");
|
||||
}
|
||||
printf("Stopping\n");
|
||||
++unrecoverable_error;
|
||||
goto bail;
|
||||
}
|
||||
|
||||
fprintf(stderr, "symlink_fixed:%s:%s: Fixed symlink error\n", disk->name, link->sub);
|
||||
++recovered_error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state_progress_end(state, countpos, countmax, countsize);
|
||||
|
||||
bail:
|
||||
|
|
|
|||
33
elem.c
33
elem.c
|
|
@ -262,6 +262,30 @@ int file_inode_compare(const void* void_arg, const void* void_data)
|
|||
return 0;
|
||||
}
|
||||
|
||||
struct snapraid_link* link_alloc(const char* sub, const char* linkto)
|
||||
{
|
||||
struct snapraid_link* link;
|
||||
|
||||
link = malloc_nofail(sizeof(struct snapraid_link));
|
||||
pathcpy(link->sub, sizeof(link->sub), sub);
|
||||
pathcpy(link->linkto, sizeof(link->linkto), linkto);
|
||||
link->flag = 0;
|
||||
|
||||
return link;
|
||||
}
|
||||
|
||||
void link_free(struct snapraid_link* link)
|
||||
{
|
||||
free(link);
|
||||
}
|
||||
|
||||
int link_name_compare(const void* void_arg, const void* void_data)
|
||||
{
|
||||
const char* arg = void_arg;
|
||||
const struct snapraid_link* link = void_data;
|
||||
return strcmp(arg, link->sub);
|
||||
}
|
||||
|
||||
struct snapraid_disk* disk_alloc(const char* name, const char* dir)
|
||||
{
|
||||
struct snapraid_disk* disk;
|
||||
|
|
@ -276,6 +300,8 @@ struct snapraid_disk* disk_alloc(const char* name, const char* dir)
|
|||
disk->first_free_block = 0;
|
||||
tommy_list_init(&disk->filelist);
|
||||
tommy_hashdyn_init(&disk->inodeset);
|
||||
tommy_list_init(&disk->linklist);
|
||||
tommy_hashdyn_init(&disk->linkset);
|
||||
tommy_array_init(&disk->blockarr);
|
||||
|
||||
return disk;
|
||||
|
|
@ -290,6 +316,13 @@ void disk_free(struct snapraid_disk* disk)
|
|||
file_free(file);
|
||||
}
|
||||
tommy_hashdyn_done(&disk->inodeset);
|
||||
node = disk->linklist;
|
||||
while (node) {
|
||||
struct snapraid_link* link = node->data;
|
||||
node = node->next;
|
||||
link_free(link);
|
||||
}
|
||||
tommy_hashdyn_done(&disk->linkset);
|
||||
tommy_array_done(&disk->blockarr);
|
||||
free(disk);
|
||||
}
|
||||
|
|
|
|||
55
elem.h
55
elem.h
|
|
@ -98,6 +98,19 @@ struct snapraid_file {
|
|||
tommy_hashdyn_node nodeset;
|
||||
};
|
||||
|
||||
/**
|
||||
* Symbolic Link.
|
||||
*/
|
||||
struct snapraid_link {
|
||||
unsigned flag; /**< FILE_IS_* flags. */
|
||||
char sub[PATH_MAX]; /**< Sub path of the file. Without the disk dir. The disk is implicit. */
|
||||
char linkto[PATH_MAX]; /**< Link to. */
|
||||
|
||||
/* nodes for data structures */
|
||||
tommy_node nodelist;
|
||||
tommy_hashdyn_node nodeset;
|
||||
};
|
||||
|
||||
/**
|
||||
* Disk.
|
||||
*/
|
||||
|
|
@ -107,6 +120,8 @@ struct snapraid_disk {
|
|||
block_off_t first_free_block; /**< First free searching block. */
|
||||
tommy_list filelist; /**< List of all the files. */
|
||||
tommy_hashdyn inodeset; /**< Hashtable by inode of all the files. */
|
||||
tommy_list linklist; /**< List of all the links. */
|
||||
tommy_hashdyn linkset; /**< Hashtable by name of all the links. */
|
||||
tommy_array blockarr; /**< Block array of the disk. */
|
||||
};
|
||||
|
||||
|
|
@ -204,7 +219,7 @@ void file_free(struct snapraid_file* file);
|
|||
const char* file_name(struct snapraid_file* file);
|
||||
|
||||
/**
|
||||
* Compares two files by inode.
|
||||
* Compares a file with an inode.
|
||||
*/
|
||||
int file_inode_compare(const void* void_arg, const void* void_data);
|
||||
|
||||
|
|
@ -216,6 +231,44 @@ static inline tommy_uint32_t file_inode_hash(uint64_t inode)
|
|||
return (tommy_uint32_t)tommy_inthash_u64(inode);
|
||||
}
|
||||
|
||||
static inline int link_flag_has(struct snapraid_link* link, unsigned mask)
|
||||
{
|
||||
return (link->flag & mask) == mask;
|
||||
}
|
||||
|
||||
static inline void link_flag_set(struct snapraid_link* link, unsigned mask)
|
||||
{
|
||||
link->flag |= mask;
|
||||
}
|
||||
|
||||
static inline void link_flag_clear(struct snapraid_link* link, unsigned mask)
|
||||
{
|
||||
link->flag &= ~mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates a link.
|
||||
*/
|
||||
struct snapraid_link* link_alloc(const char* name, const char* link);
|
||||
|
||||
/**
|
||||
* Deallocates a link.
|
||||
*/
|
||||
void link_free(struct snapraid_link* link);
|
||||
|
||||
/**
|
||||
* Compare a link with a name.
|
||||
*/
|
||||
int link_name_compare(const void* void_arg, const void* void_data);
|
||||
|
||||
/**
|
||||
* Computes the hash of a link name.
|
||||
*/
|
||||
static inline tommy_uint32_t link_name_hash(const char* name)
|
||||
{
|
||||
return tommy_hash_u32(0, name, strlen(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates a disk.
|
||||
*/
|
||||
|
|
|
|||
3
handle.c
3
handle.c
|
|
@ -195,9 +195,10 @@ int handle_open(struct snapraid_handle* handle, struct snapraid_file* file)
|
|||
return 0;
|
||||
}
|
||||
|
||||
pathprint(handle->path, sizeof(handle->path), "%s%s", handle->disk->dir, file->sub);
|
||||
|
||||
/* opening in sequential mode in Windows */
|
||||
/* do not follow links to ensure to open the real file */
|
||||
pathprint(handle->path, sizeof(handle->path), "%s%s", handle->disk->dir, file->sub);
|
||||
handle->f = open(handle->path, O_RDONLY | O_BINARY | O_SEQUENTIAL | O_NOFOLLOW);
|
||||
|
||||
if (handle->f == -1) {
|
||||
|
|
|
|||
313
mktest.c
313
mktest.c
|
|
@ -32,23 +32,29 @@ unsigned rnd(unsigned max)
|
|||
char CHARSET[] = "qwertyuiopasdfghjklzxcvbnm1234567890 .-+";
|
||||
#define CHARSET_LEN (sizeof(CHARSET)-1)
|
||||
|
||||
void rnd_name(char* file)
|
||||
{
|
||||
int l = 1 + rnd(20);
|
||||
while (l) {
|
||||
*file++ = CHARSET[rnd(CHARSET_LEN)];
|
||||
--l;
|
||||
}
|
||||
*file = 0;
|
||||
}
|
||||
|
||||
void generate(int disk, int size)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
FILE* f;
|
||||
char* file;
|
||||
int count;
|
||||
int i;
|
||||
int l;
|
||||
|
||||
snprintf(path, sizeof(path), "test/disk%d/", disk);
|
||||
i = strlen(path);
|
||||
file = path + strlen(path);
|
||||
|
||||
/* add a directory */
|
||||
if (rnd(2) == 0) {
|
||||
path[i++] = '_';
|
||||
path[i++] = 'a' + rnd(2);
|
||||
path[i] = 0;
|
||||
*file++ = '_';
|
||||
*file++ = 'a' + rnd(2);
|
||||
*file = 0;
|
||||
|
||||
/* create it */
|
||||
if (mkdir(path, 0777) != 0) {
|
||||
|
|
@ -58,14 +64,14 @@ void generate(int disk, int size)
|
|||
}
|
||||
}
|
||||
|
||||
path[i++] = '/';
|
||||
*file++ = '/';
|
||||
}
|
||||
|
||||
/* add another directory */
|
||||
if (rnd(2) == 0) {
|
||||
path[i++] = '_';
|
||||
path[i++] = 'a' + rnd(2);
|
||||
path[i] = 0;
|
||||
*file++ = '_';
|
||||
*file++ = 'a' + rnd(2);
|
||||
*file = 0;
|
||||
|
||||
/* create it */
|
||||
if (mkdir(path, 0777) != 0) {
|
||||
|
|
@ -75,145 +81,224 @@ void generate(int disk, int size)
|
|||
}
|
||||
}
|
||||
|
||||
path[i++] = '/';
|
||||
}
|
||||
|
||||
/* add a file */
|
||||
l = 1 + rnd(20);
|
||||
file = path + i;
|
||||
while (l) {
|
||||
path[i++] = CHARSET[rnd(CHARSET_LEN)];
|
||||
--l;
|
||||
}
|
||||
path[i] = 0;
|
||||
|
||||
/* skip some invalid file name, see http://en.wikipedia.org/wiki/Filename */
|
||||
if (strcmp(file, ".") == 0
|
||||
|| strcmp(file, "..") == 0
|
||||
|| strcmp(file, "prn") == 0
|
||||
|| strcmp(file, "con") == 0
|
||||
|| strcmp(file, "nul") == 0
|
||||
|| strcmp(file, "aux") == 0
|
||||
|| file[0] == ' '
|
||||
|| file[strlen(file)-1] == ' '
|
||||
|| file[strlen(file)-1] == '.'
|
||||
) {
|
||||
return;
|
||||
*file++ = '/';
|
||||
}
|
||||
|
||||
f = fopen(path, "wb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Error writing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
while (1) {
|
||||
/* add a random file */
|
||||
rnd_name(file);
|
||||
|
||||
/* skip some invalid file name, see http://en.wikipedia.org/wiki/Filename */
|
||||
if (strcmp(file, ".") == 0
|
||||
|| strcmp(file, "..") == 0
|
||||
|| strcmp(file, "prn") == 0
|
||||
|| strcmp(file, "con") == 0
|
||||
|| strcmp(file, "nul") == 0
|
||||
|| strcmp(file, "aux") == 0
|
||||
|| file[0] == ' '
|
||||
|| file[strlen(file)-1] == ' '
|
||||
|| file[strlen(file)-1] == '.'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
count = size;
|
||||
while (count) {
|
||||
fputc(rnd(256), f);
|
||||
--count;
|
||||
/* remove the existing file if any */
|
||||
if (remove(path) != 0) {
|
||||
if (errno != ENOENT) {
|
||||
fprintf(stderr, "Error removing file %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
#ifndef WIN32 /* Windows XP doesn't support symlinks */
|
||||
if (rnd(32) == 0) {
|
||||
/* symlink */
|
||||
char linkto[PATH_MAX];
|
||||
int ret;
|
||||
|
||||
rnd_name(linkto);
|
||||
|
||||
ret = symlink(linkto, path);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "Error writing symlink %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
/* file */
|
||||
FILE* f;
|
||||
int count;
|
||||
|
||||
f = fopen(path, "wb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Error writing file %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
count = size;
|
||||
while (count) {
|
||||
fputc(rnd(256), f);
|
||||
--count;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
void damage(const char* path, int size)
|
||||
{
|
||||
FILE* f;
|
||||
struct stat st;
|
||||
off_t start;
|
||||
int count;
|
||||
|
||||
if (!size)
|
||||
return;
|
||||
|
||||
f = fopen(path, "r+b");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Error writing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (fstat(fileno(f), &st) != 0) {
|
||||
if (lstat(path, &st) != 0) {
|
||||
if (errno == ENOENT)
|
||||
return; /* it may be already deleted */
|
||||
fprintf(stderr, "Error accessing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* start at random position inside the file */
|
||||
if (st.st_size)
|
||||
start = rnd(st.st_size);
|
||||
else
|
||||
start = 0;
|
||||
if (fseek(f, start, SEEK_SET) != 0) {
|
||||
fprintf(stderr, "Error seeking %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
if (S_ISLNK(st.st_mode)) {
|
||||
/* symlink */
|
||||
int ret;
|
||||
|
||||
ret = remove(path);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "Error removing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
} else {
|
||||
/* file */
|
||||
FILE* f;
|
||||
off_t start;
|
||||
int count;
|
||||
|
||||
/* write garbage, in case also over the end */
|
||||
count = rnd(size);
|
||||
while (count) {
|
||||
fputc(rnd(256), f);
|
||||
--count;
|
||||
}
|
||||
f = fopen(path, "r+b");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Error writing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
/* start at random position inside the file */
|
||||
if (st.st_size)
|
||||
start = rnd(st.st_size);
|
||||
else
|
||||
start = 0;
|
||||
if (fseek(f, start, SEEK_SET) != 0) {
|
||||
fprintf(stderr, "Error seeking %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* write garbage, in case also over the end */
|
||||
count = rnd(size);
|
||||
while (count) {
|
||||
fputc(rnd(256), f);
|
||||
--count;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
void append(const char* path, int size)
|
||||
{
|
||||
FILE* f;
|
||||
int count;
|
||||
|
||||
if (!size)
|
||||
return;
|
||||
|
||||
f = fopen(path, "ab");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Error appending %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* write garbage, in case also over the end */
|
||||
count = rnd(size);
|
||||
while (count) {
|
||||
fputc(rnd(256), f);
|
||||
--count;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
void truncat(const char* path, int size)
|
||||
{
|
||||
FILE* f;
|
||||
struct stat st;
|
||||
off_t start;
|
||||
int count;
|
||||
|
||||
if (!size)
|
||||
return;
|
||||
|
||||
f = fopen(path, "r+b");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Error writing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (fstat(fileno(f), &st) != 0) {
|
||||
if (lstat(path, &st) != 0) {
|
||||
if (errno == ENOENT)
|
||||
return; /* it may be already deleted */
|
||||
fprintf(stderr, "Error accessing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* truncate at random position inside the file */
|
||||
count = rnd(size);
|
||||
if (count > st.st_size)
|
||||
count = st.st_size;
|
||||
start = st.st_size - count;
|
||||
|
||||
if (ftruncate(fileno(f), start) != 0) {
|
||||
fprintf(stderr, "Error truncating %s\n", path);
|
||||
if (S_ISLNK(st.st_mode)) {
|
||||
/* symlink */
|
||||
int ret;
|
||||
|
||||
ret = remove(path);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "Error removing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
} else {
|
||||
/* file */
|
||||
|
||||
FILE* f;
|
||||
int count;
|
||||
|
||||
f = fopen(path, "ab");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Error appending %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* write garbage, in case also over the end */
|
||||
count = rnd(size);
|
||||
while (count) {
|
||||
fputc(rnd(256), f);
|
||||
--count;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
void truncat(const char* path, int size)
|
||||
{
|
||||
struct stat st;
|
||||
|
||||
if (!size)
|
||||
return;
|
||||
|
||||
if (lstat(path, &st) != 0) {
|
||||
if (errno == ENOENT)
|
||||
return; /* it may be already deleted */
|
||||
fprintf(stderr, "Error accessing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
if (S_ISLNK(st.st_mode)) {
|
||||
/* symlink */
|
||||
int ret;
|
||||
|
||||
ret = remove(path);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "Error removing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
} else {
|
||||
/* file */
|
||||
FILE* f;
|
||||
off_t start;
|
||||
int count;
|
||||
|
||||
f = fopen(path, "r+b");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Error writing %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* truncate at random position inside the file */
|
||||
count = rnd(size);
|
||||
if (count > st.st_size)
|
||||
count = st.st_size;
|
||||
start = st.st_size - count;
|
||||
|
||||
if (ftruncate(fileno(f), start) != 0) {
|
||||
fprintf(stderr, "Error truncating %s\n", path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
171
scan.c
171
scan.c
|
|
@ -31,7 +31,8 @@ struct snapraid_scan {
|
|||
unsigned count_remove;
|
||||
unsigned count_insert;
|
||||
|
||||
tommy_list insert_list; /**< Files to insert. */
|
||||
tommy_list file_insert_list; /**< Files to insert. */
|
||||
tommy_list link_insert_list; /**< Links to insert. */
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -228,7 +229,97 @@ static void scan_file(struct snapraid_scan* scan, struct snapraid_state* state,
|
|||
file_flag_set(file, FILE_IS_PRESENT);
|
||||
|
||||
/* insert it in the delayed insert list */
|
||||
tommy_list_insert_tail(&scan->insert_list, &file->nodelist, file);
|
||||
tommy_list_insert_tail(&scan->file_insert_list, &file->nodelist, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified link from the data set.
|
||||
*/
|
||||
static void scan_link_remove(struct snapraid_state* state, struct snapraid_disk* disk, struct snapraid_link* link)
|
||||
{
|
||||
/* state changed */
|
||||
state->need_write = 1;
|
||||
|
||||
/* remove the file from the link containers */
|
||||
tommy_hashdyn_remove_existing(&disk->linkset, &link->nodeset);
|
||||
tommy_list_remove_existing(&disk->linklist, &link->nodelist);
|
||||
|
||||
/* deallocate */
|
||||
link_free(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the specified link in the data set.
|
||||
*/
|
||||
static void scan_link_insert(struct snapraid_state* state, struct snapraid_disk* disk, struct snapraid_link* link)
|
||||
{
|
||||
/* state changed */
|
||||
state->need_write = 1;
|
||||
|
||||
/* insert the link in the link containers */
|
||||
tommy_hashdyn_insert(&disk->linkset, &link->nodeset, link, link_name_hash(link->sub));
|
||||
tommy_list_insert_tail(&disk->linklist, &link->nodelist, link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a symbolic link.
|
||||
*/
|
||||
static void scan_link(struct snapraid_scan* scan, struct snapraid_state* state, int output, struct snapraid_disk* disk, const char* sub, const char* linkto)
|
||||
{
|
||||
struct snapraid_link* link;
|
||||
|
||||
/* check if the link already exists */
|
||||
link = tommy_hashdyn_search(&disk->linkset, link_name_compare, sub, link_name_hash(sub));
|
||||
if (link) {
|
||||
/* check if multiple files have the same inode */
|
||||
if (link_flag_has(link, FILE_IS_PRESENT)) {
|
||||
fprintf(stderr, "Internal inconsistency for symlink '%s%s'\n", disk->dir, sub);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* mark as present */
|
||||
link_flag_set(link, FILE_IS_PRESENT);
|
||||
|
||||
/* check if the link is not changed */
|
||||
if (strcmp(link->linkto, linkto) == 0) {
|
||||
/* it's equal */
|
||||
++scan->count_equal;
|
||||
|
||||
if (state->gui) {
|
||||
fprintf(stderr, "scan:equal:%s:%s\n", disk->name, link->sub);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
/* nothing more to do */
|
||||
return;
|
||||
} else {
|
||||
/* it's an update */
|
||||
if (state->gui) {
|
||||
fprintf(stderr, "scan:update:%s:%s\n", disk->name, link->sub);
|
||||
fflush(stderr);
|
||||
}
|
||||
if (output) {
|
||||
printf("Update '%s%s'\n", disk->dir, link->sub);
|
||||
}
|
||||
|
||||
++scan->count_change;
|
||||
|
||||
/* update it */
|
||||
pathcpy(link->linkto, sizeof(link->linkto), linkto);
|
||||
|
||||
/* nothing more to do */
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* insert it */
|
||||
link = link_alloc(sub, linkto);
|
||||
|
||||
/* mark it as present */
|
||||
link_flag_set(link, FILE_IS_PRESENT);
|
||||
|
||||
/* insert it in the delayed insert list */
|
||||
tommy_list_insert_tail(&scan->link_insert_list, &link->nodelist, link);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -307,6 +398,30 @@ static void scan_dir(struct snapraid_scan* scan, struct snapraid_state* state, i
|
|||
printf("Excluding file '%s'\n", path_next);
|
||||
}
|
||||
}
|
||||
} else if (S_ISLNK(st.st_mode)) {
|
||||
if (filter_path(&state->filterlist, sub_next, 0) == 0) {
|
||||
char subnew[PATH_MAX];
|
||||
int ret;
|
||||
|
||||
ret = readlink(path_next, subnew, sizeof(subnew));
|
||||
if (ret >= PATH_MAX) {
|
||||
fprintf(stderr, "Error in readlink file '%s'. Symlink too long.\n", path_next);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Error in readlink file '%s'. %s.\n", path_next, strerror(errno));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* readlink doesn't put the final 0 */
|
||||
subnew[ret] = 0;
|
||||
|
||||
scan_link(scan, state, output, disk, sub_next, subnew);
|
||||
} else {
|
||||
if (state->verbose) {
|
||||
printf("Excluding file '%s'\n", path_next);
|
||||
}
|
||||
}
|
||||
} else if (S_ISDIR(st.st_mode)) {
|
||||
if (filter_path(&state->filterlist, sub_next, 1) == 0) {
|
||||
pathslash(path_next, sizeof(path_next));
|
||||
|
|
@ -347,7 +462,8 @@ void state_scan(struct snapraid_state* state, int output)
|
|||
scan[i].count_change = 0;
|
||||
scan[i].count_remove = 0;
|
||||
scan[i].count_insert = 0;
|
||||
tommy_list_init(&scan[i].insert_list);
|
||||
tommy_list_init(&scan[i].file_insert_list);
|
||||
tommy_list_init(&scan[i].link_insert_list);
|
||||
}
|
||||
|
||||
for(i=0;i<diskmax;++i) {
|
||||
|
|
@ -382,8 +498,32 @@ void state_scan(struct snapraid_state* state, int output)
|
|||
}
|
||||
}
|
||||
|
||||
/* check for removed links */
|
||||
node = disk->linklist;
|
||||
while (node) {
|
||||
struct snapraid_link* link = node->data;
|
||||
|
||||
/* next node */
|
||||
node = node->next;
|
||||
|
||||
/* remove if not present */
|
||||
if (!link_flag_has(link, FILE_IS_PRESENT)) {
|
||||
++scan[i].count_remove;
|
||||
|
||||
if (state->gui) {
|
||||
fprintf(stderr, "scan:remove:%s:%s\n", disk->name, link->sub);
|
||||
fflush(stderr);
|
||||
}
|
||||
if (output) {
|
||||
printf("Remove '%s%s'\n", disk->dir, link->sub);
|
||||
}
|
||||
|
||||
scan_link_remove(state, disk, link);
|
||||
}
|
||||
}
|
||||
|
||||
/* insert all the new files, we insert them only after the deletion */
|
||||
node = scan[i].insert_list;
|
||||
node = scan[i].file_insert_list;
|
||||
while (node) {
|
||||
struct snapraid_file* file = node->data;
|
||||
|
||||
|
|
@ -405,6 +545,29 @@ void state_scan(struct snapraid_state* state, int output)
|
|||
scan_file_insert(state, disk, file);
|
||||
}
|
||||
|
||||
/* insert all the new links */
|
||||
node = scan[i].link_insert_list;
|
||||
while (node) {
|
||||
struct snapraid_link* link = node->data;
|
||||
|
||||
/* next node */
|
||||
node = node->next;
|
||||
|
||||
/* create the new link */
|
||||
++scan->count_insert;
|
||||
|
||||
if (state->gui) {
|
||||
fprintf(stderr, "scan:add:%s:%s\n", disk->name, link->sub);
|
||||
fflush(stderr);
|
||||
}
|
||||
if (output) {
|
||||
printf("Add '%s%s'\n", disk->dir, link->sub);
|
||||
}
|
||||
|
||||
/* insert it */
|
||||
scan_link_insert(state, disk, link);
|
||||
}
|
||||
|
||||
/* if all the previous file were removed */
|
||||
if (scan[i].count_equal == 0 && scan[i].count_moved == 0 && scan[i].count_remove != 0) {
|
||||
if (!state->force_empty) {
|
||||
|
|
|
|||
|
|
@ -101,8 +101,8 @@ It\(cqs able to recover damages only from up to two disks.
|
|||
With a Backup you are able to recover from a complete
|
||||
failure of the whole disk array.
|
||||
.IP \(bu
|
||||
Only the file data is saved. Permissions, time, extended attributes,
|
||||
hard\(hylinks, symbolic\(hylinks are not saved.
|
||||
Only file data and symlinks are saved. Permissions, time, extended attributes,
|
||||
hard\(hylinks are not saved.
|
||||
.PD
|
||||
.SH GETTING STARTED
|
||||
To use SnapRAID you need first select one disk of your disk array
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ Limitations
|
|||
* It's able to recover damages only from up to two disks.
|
||||
With a Backup you are able to recover from a complete
|
||||
failure of the whole disk array.
|
||||
* Only the file data is saved. Permissions, time, extended attributes,
|
||||
hard-links, symbolic-links are not saved.
|
||||
* Only file data and symlinks are saved. Permissions, time, extended attributes,
|
||||
hard-links are not saved.
|
||||
|
||||
Getting Started
|
||||
To use SnapRAID you need first select one disk of your disk array
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ Other limitations are:
|
|||
* It's able to recover damages only from up to two disks.
|
||||
With a Backup you are able to recover from a complete
|
||||
failure of the whole disk array.
|
||||
* Only the file data is saved. Permissions, time, extended attributes,
|
||||
hard-links, symbolic-links are not saved.
|
||||
* Only file data and symlinks are saved. Permissions, time, extended attributes,
|
||||
hard-links are not saved.
|
||||
|
||||
|
||||
4 GETTING STARTED
|
||||
|
|
|
|||
121
state.c
121
state.c
|
|
@ -287,10 +287,12 @@ void state_read(struct snapraid_state* state)
|
|||
unsigned line;
|
||||
unsigned count_file;
|
||||
unsigned count_block;
|
||||
unsigned count_link;
|
||||
tommy_node* node;
|
||||
|
||||
count_file = 0;
|
||||
count_block = 0;
|
||||
count_link = 0;
|
||||
|
||||
/* iterate over all the available content files and load the first one present */
|
||||
f = 0;
|
||||
|
|
@ -435,7 +437,6 @@ void state_read(struct snapraid_state* state)
|
|||
++count_block;
|
||||
} else if (strcmp(tag, "file") == 0) {
|
||||
/* file */
|
||||
char buffer[PATH_MAX];
|
||||
char sub[PATH_MAX];
|
||||
uint64_t v_size;
|
||||
uint64_t v_mtime;
|
||||
|
|
@ -535,6 +536,86 @@ void state_read(struct snapraid_state* state)
|
|||
|
||||
/* stat */
|
||||
++count_file;
|
||||
} else if (strcmp(tag, "symlink") == 0) {
|
||||
/* symlink */
|
||||
char sub[PATH_MAX];
|
||||
char linkto[PATH_MAX];
|
||||
char tokento[32];
|
||||
unsigned i;
|
||||
struct snapraid_link* link;
|
||||
|
||||
ret = sgettok(f, buffer, sizeof(buffer));
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Invalid 'symlink' specification in '%s' at line %u\n", path, line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
c = sgetc(f);
|
||||
if (c != ' ') {
|
||||
fprintf(stderr, "Invalid 'symlink' specification in '%s' at line %u\n", path, line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
ret = sgetline(f, sub, sizeof(sub));
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Invalid 'symlink' specification in '%s' at line %u\n", path, line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
c = sgeteol(f);
|
||||
if (c != '\n') {
|
||||
fprintf(stderr, "Invalid 'symlink' specification in '%s' at line %u\n", path, line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
++line;
|
||||
|
||||
ret = sgettok(f, tokento, sizeof(tokento));
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Invalid 'symlink' specification in '%s' at line %u\n", path, line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
if (strcmp(tokento, "to") != 0) {
|
||||
fprintf(stderr, "Invalid 'symlink' specification in '%s' at line %u\n", path, line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
c = sgetc(f);
|
||||
if (c != ' ') {
|
||||
fprintf(stderr, "Invalid 'symlink' specification in '%s' at line %u\n", path, line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
ret = sgetline(f, linkto, sizeof(linkto));
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Invalid 'symlink' specification in '%s' at line %u\n", path, line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (!*sub || !*linkto) {
|
||||
fprintf(stderr, "Invalid 'symlink' specification in '%s' at line %u\n", path, line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* find the disk */
|
||||
for(i=0;i<tommy_array_size(&state->diskarr);++i) {
|
||||
disk = tommy_array_get(&state->diskarr, i);
|
||||
if (strcmp(disk->name, buffer) == 0)
|
||||
break;
|
||||
}
|
||||
if (i == tommy_array_size(&state->diskarr)) {
|
||||
fprintf(stderr, "Disk named '%s' not found in '%s' at line %u\n", buffer, path, line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* allocate the link */
|
||||
link = link_alloc(sub, linkto);
|
||||
|
||||
/* insert the link in the link containers */
|
||||
tommy_hashdyn_insert(&disk->linkset, &link->nodeset, link, link_name_hash(link->sub));
|
||||
tommy_list_insert_tail(&disk->linklist, &link->nodelist, link);
|
||||
|
||||
/* stat */
|
||||
++count_link;
|
||||
} else if (strcmp(tag, "checksum") == 0) {
|
||||
ret = sgettok(f, buffer, sizeof(buffer));
|
||||
if (ret < 0) {
|
||||
|
|
@ -605,20 +686,23 @@ void state_read(struct snapraid_state* state)
|
|||
if (state->verbose) {
|
||||
printf("\tfile %u\n", count_file);
|
||||
printf("\tblock %u\n", count_block);
|
||||
printf("\tsymlink %u\n", count_link);
|
||||
}
|
||||
}
|
||||
|
||||
static void state_write_one(struct snapraid_state* state, const char* path, unsigned* out_count_file, unsigned* out_count_block)
|
||||
static void state_write_one(struct snapraid_state* state, const char* path, unsigned* out_count_file, unsigned* out_count_block, unsigned* out_count_link)
|
||||
{
|
||||
FILE* f;
|
||||
char tmp[PATH_MAX];
|
||||
unsigned count_file;
|
||||
unsigned count_block;
|
||||
unsigned count_link;
|
||||
unsigned i;
|
||||
int ret;
|
||||
|
||||
count_file = 0;
|
||||
count_block = 0;
|
||||
count_link = 0;
|
||||
|
||||
printf("Saving state to %s...\n", path);
|
||||
|
||||
|
|
@ -697,6 +781,19 @@ static void state_write_one(struct snapraid_state* state, const char* path, unsi
|
|||
|
||||
++count_file;
|
||||
}
|
||||
|
||||
/* for each link */
|
||||
for(j=disk->linklist;j!=0;j=j->next) {
|
||||
struct snapraid_link* link = j->data;
|
||||
|
||||
ret = fprintf(f,"symlink %s %s\nto %s\n", disk->name, link->sub, link->linkto);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Error writing the content file '%s' in fprintf(). %s.\n", tmp, strerror(errno));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
++count_link;
|
||||
}
|
||||
}
|
||||
|
||||
/* Use the sequence fflush() -> fsync() -> fclose() -> rename() to ensure */
|
||||
|
|
@ -728,27 +825,32 @@ static void state_write_one(struct snapraid_state* state, const char* path, unsi
|
|||
*out_count_file = count_file;
|
||||
if (out_count_block)
|
||||
*out_count_block = count_block;
|
||||
if (out_count_link)
|
||||
*out_count_link = count_link;
|
||||
}
|
||||
|
||||
void state_write(struct snapraid_state* state)
|
||||
{
|
||||
unsigned count_file;
|
||||
unsigned count_block;
|
||||
unsigned count_link;
|
||||
tommy_node* node;
|
||||
|
||||
count_file = 0;
|
||||
count_block = 0;
|
||||
count_link = 0;
|
||||
|
||||
node = tommy_list_head(&state->contentlist);
|
||||
while (node) {
|
||||
struct snapraid_content* content = node->data;
|
||||
state_write_one(state, content->content, &count_file, &count_block);
|
||||
state_write_one(state, content->content, &count_file, &count_block, &count_link);
|
||||
node = node->next;
|
||||
}
|
||||
|
||||
if (state->verbose) {
|
||||
printf("\tfile %u\n", count_file);
|
||||
printf("\tblock %u\n", count_block);
|
||||
printf("\tsymlink %u\n", count_link);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -790,6 +892,19 @@ void state_filter(struct snapraid_state* state, tommy_list* filterlist)
|
|||
printf("Processing file '%s'\n", file->sub);
|
||||
}
|
||||
}
|
||||
|
||||
/* for each link */
|
||||
for(j=tommy_list_head(&disk->linklist);j!=0;j=j->next) {
|
||||
struct snapraid_link* link = j->data;
|
||||
|
||||
if (filter_path(filterlist, link->sub, 0) != 0) {
|
||||
link_flag_set(link, FILE_IS_EXCLUDED);
|
||||
}
|
||||
|
||||
if (state->verbose && !link_flag_has(link, FILE_IS_EXCLUDED)) {
|
||||
printf("Processing symlink '%s'\n", link->sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue