/*------------------------------------------------------------------------- * * restore.c: restore DB cluster and archived WAL. * * Portions Copyright (c) 2009-2013, NIPPON TELEGRAPH AND TELEPHONE CORPORATION * Portions Copyright (c) 2015-2017, Postgres Professional * *------------------------------------------------------------------------- */ #include "pg_probackup.h" #include #include #include #include #include #include "catalog/pg_control.h" typedef struct { parray *files; pgBackup *backup; } restore_files_args; /* Tablespace mapping structures */ typedef struct TablespaceListCell { struct TablespaceListCell *next; char old_dir[MAXPGPATH]; char new_dir[MAXPGPATH]; } TablespaceListCell; typedef struct TablespaceList { TablespaceListCell *head; TablespaceListCell *tail; } TablespaceList; typedef struct TablespaceCreatedListCell { struct TablespaceCreatedListCell *next; char link_name[MAXPGPATH]; char linked_dir[MAXPGPATH]; } TablespaceCreatedListCell; typedef struct TablespaceCreatedList { TablespaceCreatedListCell *head; TablespaceCreatedListCell *tail; } TablespaceCreatedList; static void restore_backup(pgBackup *backup); static void restore_directories(const char *pg_data_dir, const char *backup_dir); static void check_tablespace_mapping(pgBackup *backup); static void create_recovery_conf(time_t backup_id, const char *target_time, const char *target_xid, const char *target_inclusive, TimeLineID target_tli); static void restore_files(void *arg); static void remove_deleted_files(pgBackup *backup); static const char *get_tablespace_mapping(const char *dir); static void set_tablespace_created(const char *link, const char *dir); static const char *get_tablespace_created(const char *link); /* Tablespace mapping */ static TablespaceList tablespace_dirs = {NULL, NULL}; static TablespaceCreatedList tablespace_created_dirs = {NULL, NULL}; /* * Entry point of pg_probackup RESTORE and VALIDATE subcommands. */ int do_restore_or_validate(time_t target_backup_id, const char *target_time, const char *target_xid, const char *target_inclusive, TimeLineID target_tli, bool is_restore) { int i; parray *backups; parray *timelines; pgRecoveryTarget *rt = NULL; pgBackup *current_backup = NULL; pgBackup *dest_backup = NULL; pgBackup *base_full_backup = NULL; int dest_backup_index; int base_full_backup_index; char *action = is_restore ? "Restore":"Validate"; if (is_restore) { if (pgdata == NULL) elog(ERROR, "required parameter not specified: PGDATA (-D, --pgdata)"); /* Check if restore destination empty */ if (!dir_is_empty(pgdata)) elog(ERROR, "restore destination is not empty: \"%s\"", pgdata); } rt = parseRecoveryTargetOptions(target_time, target_xid, target_inclusive); elog(LOG, "%s begin.", action); /* Get exclusive lock of backup catalog */ catalog_lock(); /* Get list of all backups sorted in order of descending start time */ backups = catalog_get_backup_list(INVALID_BACKUP_ID); if (backups == NULL) elog(ERROR, "Failed to get backup list."); if (target_tli) { elog(LOG, "target timeline ID = %u", target_tli); /* Read timeline history files from archives */ timelines = readTimeLineHistory_probackup(target_tli); } /* Find backup range we should restore. */ for (i = 0; i < parray_num(backups); i++) { current_backup = (pgBackup *) parray_get(backups, i); /* Skip all backups which started after target backup */ if (target_backup_id && current_backup->start_time > target_backup_id) continue; /* * We found target backup. Check its status and * ensure that it satisfies recovery target. */ if (target_backup_id == current_backup->start_time || target_backup_id == INVALID_BACKUP_ID) { if (current_backup->status != BACKUP_STATUS_OK) elog(ERROR, "given backup %s is in %s status", base36enc(target_backup_id), status2str(current_backup->status)); if (target_tli) { if (!satisfy_timeline(timelines, current_backup)) { if (target_backup_id != INVALID_BACKUP_ID) elog(ERROR, "target backup %s does not satisfy target timeline", base36enc(target_backup_id)); else /* Try to find another backup that satisfies target timeline */ continue; } } if (!satisfy_recovery_target(current_backup, rt)) { if (target_backup_id != INVALID_BACKUP_ID) elog(ERROR, "target backup %s does not satisfy restore options", base36enc(target_backup_id)); else /* Try to find another backup that satisfies target options */ continue; } /* * Backup is fine and satisfies all recovery options. * Save it as dest_backup */ dest_backup = current_backup; dest_backup_index = i; } /* If we already found dest_backup, look for full backup. */ if (dest_backup) { if (current_backup->backup_mode == BACKUP_MODE_FULL) { if (current_backup->status != BACKUP_STATUS_OK) elog(ERROR, "base backup %s for given backup %s is in %s status", base36enc(current_backup->start_time), base36enc(dest_backup->start_time), status2str(current_backup->status)); else { /* We found both dest and base backups. */ base_full_backup = current_backup; base_full_backup_index = i; break; } } else /* Skip differential backups are ok */ continue; } } if (base_full_backup == NULL) elog(ERROR, "Full backup satisfying target options is not found."); /* * Ensure that directories provided in tablespace mapping are valid * i.e. empty or not exist. */ if (is_restore) check_tablespace_mapping(dest_backup); /* * Validate backups from base_full_backup to dest_backup. * And restore if subcommand is RESTORE. */ for (i = base_full_backup_index; i >= dest_backup_index; i--) { pgBackup *backup = (pgBackup *) parray_get(backups, i); if (backup->status == BACKUP_STATUS_OK) { pgBackupValidate(backup); if (is_restore) restore_backup(backup); } } /* * Delete files which are not in dest backup file list. Files which were * deleted between previous and current backup are not in the list. */ if (is_restore) { pgBackup *dest_backup = (pgBackup *) parray_get(backups, dest_backup_index); if (dest_backup->backup_mode != BACKUP_MODE_FULL) remove_deleted_files(dest_backup); } if (!dest_backup->stream || (target_time != NULL || target_xid != NULL)) { if (is_restore) create_recovery_conf(target_backup_id, target_time, target_xid, target_inclusive, target_tli); else validate_wal(dest_backup, arclog_path, rt->recovery_target_time, rt->recovery_target_xid, base_full_backup->tli); } /* cleanup */ parray_walk(backups, pgBackupFree); parray_free(backups); elog(LOG, "%s completed.", action); return 0; } /* * Restore one backup. */ void restore_backup(pgBackup *backup) { char timestamp[100]; char backup_path[MAXPGPATH]; char database_path[MAXPGPATH]; char list_path[MAXPGPATH]; parray *files; int i; pthread_t restore_threads[num_threads]; restore_files_args *restore_threads_args[num_threads]; /* confirm block size compatibility */ if (backup->block_size != BLCKSZ) elog(ERROR, "BLCKSZ(%d) is not compatible(%d expected)", backup->block_size, BLCKSZ); if (backup->wal_block_size != XLOG_BLCKSZ) elog(ERROR, "XLOG_BLCKSZ(%d) is not compatible(%d expected)", backup->wal_block_size, XLOG_BLCKSZ); time2iso(timestamp, lengthof(timestamp), backup->start_time); elog(LOG, "restoring database from backup %s", timestamp); /* * Restore backup directories. */ pgBackupGetPath(backup, backup_path, lengthof(backup_path), NULL); restore_directories(pgdata, backup_path); /* * Get list of files which need to be restored. */ pgBackupGetPath(backup, database_path, lengthof(database_path), DATABASE_DIR); pgBackupGetPath(backup, list_path, lengthof(list_path), DATABASE_FILE_LIST); files = dir_read_file_list(database_path, list_path); for (i = parray_num(files) - 1; i >= 0; i--) { pgFile *file = (pgFile *) parray_get(files, i); /* * Remove files which haven't changed since previous backup * and was not backed up */ if (file->write_size == BYTES_INVALID) pgFileFree(parray_remove(files, i)); } /* setup threads */ for (i = 0; i < parray_num(files); i++) { pgFile *file = (pgFile *) parray_get(files, i); __sync_lock_release(&file->lock); } /* Restore files into target directory */ for (i = 0; i < num_threads; i++) { restore_files_args *arg = pg_malloc(sizeof(restore_files_args)); arg->files = files; arg->backup = backup; if (verbose) elog(LOG, "Start thread for num:%li", parray_num(files)); restore_threads_args[i] = arg; pthread_create(&restore_threads[i], NULL, (void *(*)(void *)) restore_files, arg); } /* Wait theads */ for (i = 0; i < num_threads; i++) { pthread_join(restore_threads[i], NULL); pg_free(restore_threads_args[i]); } /* cleanup */ parray_walk(files, pgFileFree); parray_free(files); /* TODO print backup name */ elog(LOG, "restore backup completed"); } /* * Delete files which are not in backup's file list from target pgdata. * It is necessary to restore incremental backup correctly. * Files which were deleted between previous and current backup * are not in the backup's filelist. */ static void remove_deleted_files(pgBackup *backup) { parray *files; parray *files_restored; char database_path[MAXPGPATH]; char filelist_path[MAXPGPATH]; int i; pgBackupGetPath(backup, database_path, lengthof(database_path), DATABASE_DIR); pgBackupGetPath(backup, filelist_path, lengthof(filelist_path), DATABASE_FILE_LIST); /* Read backup's filelist using target database path as base path */ files = dir_read_file_list(pgdata, filelist_path); parray_qsort(files, pgFileComparePathDesc); /* Get list of files actually existing in target database */ files_restored = parray_new(); dir_list_file(files_restored, pgdata, true, true, false); /* To delete from leaf, sort in reversed order */ parray_qsort(files_restored, pgFileComparePathDesc); for (i = 0; i < parray_num(files_restored); i++) { pgFile *file = (pgFile *) parray_get(files_restored, i); /* If the file is not in the file list, delete it */ if (parray_bsearch(files, file, pgFileComparePathDesc) == NULL) { pgFileDelete(file); if (verbose) elog(LOG, "deleted %s", GetRelativePath(file->path, pgdata)); } } /* cleanup */ parray_walk(files, pgFileFree); parray_free(files); parray_walk(files_restored, pgFileFree); parray_free(files_restored); } /* * Restore backup directories from **backup_database_dir** to **pg_data_dir**. * * TODO: Think about simplification and clarity of the function. */ static void restore_directories(const char *pg_data_dir, const char *backup_dir) { parray *dirs, *links; size_t i; char backup_database_dir[MAXPGPATH], to_path[MAXPGPATH]; dirs = parray_new(); links = parray_new(); join_path_components(backup_database_dir, backup_dir, DATABASE_DIR); list_data_directories(dirs, backup_database_dir, true, false); read_tablespace_map(links, backup_dir); elog(LOG, "restore directories and symlinks..."); for (i = 0; i < parray_num(dirs); i++) { pgFile *dir = (pgFile *) parray_get(dirs, i); char *relative_ptr = GetRelativePath(dir->path, backup_database_dir); Assert(S_ISDIR(dir->mode)); /* First try to create symlink and linked directory */ if (path_is_prefix_of_path(PG_TBLSPC_DIR, relative_ptr)) { char *link_ptr = GetRelativePath(relative_ptr, PG_TBLSPC_DIR), *link_sep, *tmp_ptr; char link_name[MAXPGPATH]; pgFile **link; /* Extract link name from relative path */ link_sep = first_dir_separator(link_ptr); if (link_sep) { int len = link_sep - link_ptr; strncpy(link_name, link_ptr, len); link_name[len] = '\0'; } else goto create_directory; tmp_ptr = dir->path; dir->path = link_name; /* Search only by symlink name without path */ link = (pgFile **) parray_bsearch(links, dir, pgFileComparePath); dir->path = tmp_ptr; if (link) { const char *linked_path = get_tablespace_mapping((*link)->linked); const char *dir_created; if (!is_absolute_path(linked_path)) elog(ERROR, "tablespace directory is not an absolute path: %s\n", linked_path); /* Check if linked directory was created earlier */ dir_created = get_tablespace_created(link_name); if (dir_created) { /* * If symlink and linked directory were created do not * create it second time. */ if (strcmp(dir_created, linked_path) == 0) { /* Create rest of directories */ if (link_sep && (link_sep + 1)) goto create_directory; else continue; } else elog(ERROR, "tablespace directory \"%s\" of page backup does not " "match with previous created tablespace directory \"%s\" of symlink \"%s\"", linked_path, dir_created, link_name); } /* * This check was done in check_tablespace_mapping(). But do * it again. */ if (!dir_is_empty(linked_path)) elog(ERROR, "restore tablespace destination is not empty: \"%s\"", linked_path); if (link_sep) elog(LOG, "create directory \"%s\" and symbolic link \"%.*s\"", linked_path, (int) (link_sep - relative_ptr), relative_ptr); else elog(LOG, "create directory \"%s\" and symbolic link \"%s\"", linked_path, relative_ptr); /* Firstly, create linked directory */ dir_create_dir(linked_path, DIR_PERMISSION); join_path_components(to_path, pg_data_dir, PG_TBLSPC_DIR); /* Create pg_tblspc directory just in case */ dir_create_dir(to_path, DIR_PERMISSION); /* Secondly, create link */ join_path_components(to_path, to_path, link_name); if (symlink(linked_path, to_path) < 0) elog(ERROR, "could not create symbolic link \"%s\": %s", to_path, strerror(errno)); /* Save linked directory */ set_tablespace_created(link_name, linked_path); /* Create rest of directories */ if (link_sep && (link_sep + 1)) goto create_directory; continue; } } create_directory: elog(LOG, "create directory \"%s\"", relative_ptr); /* This is not symlink, create directory */ join_path_components(to_path, pg_data_dir, relative_ptr); dir_create_dir(to_path, DIR_PERMISSION); } parray_walk(links, pgFileFree); parray_free(links); parray_walk(dirs, pgFileFree); parray_free(dirs); } /* * Check that all tablespace mapping entries have correct linked directory * paths. Linked directories should be empty or do not exist. * * If tablespace-mapping option is supplied all OLDDIR entries should have * entries in tablespace_map file. * TODO review */ static void check_tablespace_mapping(pgBackup *backup) { char backup_path[MAXPGPATH]; parray *links; size_t i; TablespaceListCell *cell; pgFile *tmp_file = pgut_new(pgFile); links = parray_new(); pgBackupGetPath(backup, backup_path, lengthof(backup_path), NULL); read_tablespace_map(links, backup_path); elog(LOG, "check tablespace directories..."); /* 1 - OLDDIR should has an entry in links */ for (cell = tablespace_dirs.head; cell; cell = cell->next) { tmp_file->linked = cell->old_dir; if (parray_bsearch(links, tmp_file, pgFileCompareLinked) == NULL) elog(ERROR, "--tablespace-mapping option's old directory " "has not an entry in tablespace_map file: \"%s\"", cell->old_dir); } /* 2 - all linked directories should be empty */ for (i = 0; i < parray_num(links); i++) { pgFile *link = (pgFile *) parray_get(links, i); const char *linked_path = link->linked; TablespaceListCell *cell; for (cell = tablespace_dirs.head; cell; cell = cell->next) if (strcmp(link->linked, cell->old_dir) == 0) { linked_path = cell->new_dir; break; } if (!is_absolute_path(linked_path)) elog(ERROR, "tablespace directory is not an absolute path: %s\n", linked_path); if (!dir_is_empty(linked_path)) elog(ERROR, "restore tablespace destination is not empty: \"%s\"", linked_path); } free(tmp_file); parray_walk(links, pgFileFree); parray_free(links); } /* * Restore files into $PGDATA. */ static void restore_files(void *arg) { int i; restore_files_args *arguments = (restore_files_args *)arg; for (i = 0; i < parray_num(arguments->files); i++) { char from_root[MAXPGPATH]; char *rel_path; pgFile *file = (pgFile *) parray_get(arguments->files, i); if (__sync_lock_test_and_set(&file->lock, 1) != 0) continue; pgBackupGetPath(arguments->backup, from_root, lengthof(from_root), DATABASE_DIR); /* check for interrupt */ if (interrupted) elog(ERROR, "interrupted during restore database"); rel_path = GetRelativePath(file->path,from_root); if (progress) elog(LOG, "Progress: (%d/%lu). Process file %s ", i + 1, (unsigned long) parray_num(arguments->files), rel_path); /* Directories are created before */ if (S_ISDIR(file->mode)) { elog(LOG, "directory, skip"); continue; } /* not backed up */ if (file->write_size == BYTES_INVALID) { elog(LOG, "not backed up, skip"); continue; } /* Do not restore tablespace_map file */ if (path_is_prefix_of_path(PG_TABLESPACE_MAP_FILE, rel_path)) { elog(LOG, "skip tablespace_map"); continue; } /* restore file */ restore_data_file(from_root, pgdata, file, arguments->backup); /* print size of restored file */ elog(LOG, "restored %lu\n", (unsigned long) file->write_size); } } static void create_recovery_conf(time_t backup_id, const char *target_time, const char *target_xid, const char *target_inclusive, TimeLineID target_tli) { char path[MAXPGPATH]; FILE *fp; elog(LOG, "----------------------------------------"); elog(LOG, "creating recovery.conf"); snprintf(path, lengthof(path), "%s/recovery.conf", pgdata); fp = fopen(path, "wt"); if (fp == NULL) elog(ERROR, "cannot open recovery.conf \"%s\": %s", path, strerror(errno)); fprintf(fp, "# recovery.conf generated by pg_probackup %s\n", PROGRAM_VERSION); fprintf(fp, "restore_command = 'cp %s/%%f %%p'\n", arclog_path); if (target_time) fprintf(fp, "recovery_target_time = '%s'\n", target_time); else if (target_xid) fprintf(fp, "recovery_target_xid = '%s'\n", target_xid); else if (backup_id != 0) { /* TODO Why does it depend on backup_id? */ fprintf(fp, "recovery_target = 'immediate'\n"); fprintf(fp, "recovery_target_action = 'promote'\n"); } if (target_inclusive) fprintf(fp, "recovery_target_inclusive = '%s'\n", target_inclusive); if (target_tli) fprintf(fp, "recovery_target_timeline = '%u'\n", target_tli); fclose(fp); } /* * Try to read a timeline's history file. * * If successful, return the list of component TLIs (the ancestor * timelines followed by target timeline). If we cannot find the history file, * assume that the timeline has no parents, and return a list of just the * specified timeline ID. * based on readTimeLineHistory() in timeline.c */ parray * readTimeLineHistory_probackup(TimeLineID targetTLI) { parray *result; char path[MAXPGPATH]; char fline[MAXPGPATH]; FILE *fd; TimeLineHistoryEntry *entry; TimeLineHistoryEntry *last_timeline = NULL; /* Look for timeline history file in archlog_path */ snprintf(path, lengthof(path), "%s/%08X.history", arclog_path, targetTLI); fd = fopen(path, "rt"); if (fd == NULL) { if (errno != ENOENT) elog(ERROR, "could not open file \"%s\": %s", path, strerror(errno)); } result = parray_new(); /* * Parse the file... */ while (fgets(fline, sizeof(fline), fd) != NULL) { char *ptr; TimeLineID tli; uint32 switchpoint_hi; uint32 switchpoint_lo; int nfields; for (ptr = fline; *ptr; ptr++) { if (!isspace((unsigned char) *ptr)) break; } if (*ptr == '\0' || *ptr == '#') continue; nfields = sscanf(fline, "%u\t%X/%X", &tli, &switchpoint_hi, &switchpoint_lo); if (nfields < 1) { /* expect a numeric timeline ID as first field of line */ elog(ERROR, "syntax error in history file: %s. Expected a numeric timeline ID.", fline); } if (nfields != 3) elog(ERROR, "syntax error in history file: %s. Expected a transaction log switchpoint location.", fline); if (last_timeline && tli <= last_timeline->tli) elog(ERROR, "Timeline IDs must be in increasing sequence."); entry = pgut_new(TimeLineHistoryEntry); entry->tli = tli; entry->end = ((uint64) switchpoint_hi << 32) | switchpoint_lo; last_timeline = entry; /* Build list with newest item first */ parray_insert(result, 0, entry); /* we ignore the remainder of each line */ } if (fd) fclose(fd); if (last_timeline && targetTLI <= last_timeline->tli) elog(ERROR, "Timeline IDs must be less than child timeline's ID."); /* append target timeline */ entry = pgut_new(TimeLineHistoryEntry); entry->tli = targetTLI; /* lsn in target timeline is valid */ entry->end = (uint32) (-1UL << 32) | -1UL; parray_insert(result, 0, entry); return result; } bool satisfy_recovery_target(const pgBackup *backup, const pgRecoveryTarget *rt) { if (rt->xid_specified) return backup->recovery_xid <= rt->recovery_target_xid; if (rt->time_specified) return backup->recovery_time <= rt->recovery_target_time; return true; } bool satisfy_timeline(const parray *timelines, const pgBackup *backup) { int i; for (i = 0; i < parray_num(timelines); i++) { TimeLineHistoryEntry *timeline = (TimeLineHistoryEntry *) parray_get(timelines, i); if (backup->tli == timeline->tli && backup->stop_lsn < timeline->end) return true; } return false; } /* * Get recovery options in the string format, parse them * and fill up the pgRecoveryTarget structure. * TODO move arguments parsing and validation to getopt. */ pgRecoveryTarget * parseRecoveryTargetOptions(const char *target_time, const char *target_xid, const char *target_inclusive) { time_t dummy_time; TransactionId dummy_xid; bool dummy_bool; pgRecoveryTarget *rt; /* Initialize pgRecoveryTarget */ rt = pgut_new(pgRecoveryTarget); rt->time_specified = false; rt->xid_specified = false; rt->recovery_target_time = 0; rt->recovery_target_xid = 0; rt->recovery_target_inclusive = false; if (target_time) { rt->time_specified = true; if (parse_time(target_time, &dummy_time)) rt->recovery_target_time = dummy_time; else elog(ERROR, "Invalid value of --time option %s", target_time); } if (target_xid) { rt->xid_specified = true; #ifdef PGPRO_EE if (parse_uint64(target_xid, &dummy_xid)) #else if (parse_uint32(target_xid, &dummy_xid)) #endif rt->recovery_target_xid = dummy_xid; else elog(ERROR, "Invalid value of --xid option %s", target_xid); } if (target_inclusive) { if (parse_bool(target_inclusive, &dummy_bool)) rt->recovery_target_inclusive = dummy_bool; else elog(ERROR, "Invalid value of --inclusive option %s", target_inclusive); } return rt; } /* * Split argument into old_dir and new_dir and append to tablespace mapping * list. * * Copy of function tablespace_list_append() from pg_basebackup.c. */ void opt_tablespace_map(pgut_option *opt, const char *arg) { TablespaceListCell *cell = pgut_new(TablespaceListCell); char *dst; char *dst_ptr; const char *arg_ptr; dst_ptr = dst = cell->old_dir; for (arg_ptr = arg; *arg_ptr; arg_ptr++) { if (dst_ptr - dst >= MAXPGPATH) elog(ERROR, "directory name too long"); if (*arg_ptr == '\\' && *(arg_ptr + 1) == '=') ; /* skip backslash escaping = */ else if (*arg_ptr == '=' && (arg_ptr == arg || *(arg_ptr - 1) != '\\')) { if (*cell->new_dir) elog(ERROR, "multiple \"=\" signs in tablespace mapping\n"); else dst = dst_ptr = cell->new_dir; } else *dst_ptr++ = *arg_ptr; } if (!*cell->old_dir || !*cell->new_dir) elog(ERROR, "invalid tablespace mapping format \"%s\", " "must be \"OLDDIR=NEWDIR\"", arg); /* * This check isn't absolutely necessary. But all tablespaces are created * with absolute directories, so specifying a non-absolute path here would * just never match, possibly confusing users. It's also good to be * consistent with the new_dir check. */ if (!is_absolute_path(cell->old_dir)) elog(ERROR, "old directory is not an absolute path in tablespace mapping: %s\n", cell->old_dir); if (!is_absolute_path(cell->new_dir)) elog(ERROR, "new directory is not an absolute path in tablespace mapping: %s\n", cell->new_dir); if (tablespace_dirs.tail) tablespace_dirs.tail->next = cell; else tablespace_dirs.head = cell; tablespace_dirs.tail = cell; } /* * Retrieve tablespace path, either relocated or original depending on whether * -T was passed or not. * * Copy of function get_tablespace_mapping() from pg_basebackup.c. */ static const char * get_tablespace_mapping(const char *dir) { TablespaceListCell *cell; for (cell = tablespace_dirs.head; cell; cell = cell->next) if (strcmp(dir, cell->old_dir) == 0) return cell->new_dir; return dir; } /* * Save create directory path into memory. We can use it in next page restore to * not raise the error "restore tablespace destination is not empty" in * restore_directories(). */ static void set_tablespace_created(const char *link, const char *dir) { TablespaceCreatedListCell *cell = pgut_new(TablespaceCreatedListCell); strcpy(cell->link_name, link); strcpy(cell->linked_dir, dir); if (tablespace_created_dirs.tail) tablespace_created_dirs.tail->next = cell; else tablespace_created_dirs.head = cell; tablespace_created_dirs.tail = cell; } /* * Is directory was created when symlink was created in restore_directories(). */ static const char * get_tablespace_created(const char *link) { TablespaceCreatedListCell *cell; for (cell = tablespace_created_dirs.head; cell; cell = cell->next) if (strcmp(link, cell->link_name) == 0) return cell->linked_dir; return NULL; }