/*------------------------------------------------------------------------- * * bbstreamer_file.c * * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/bbstreamer_file.c *------------------------------------------------------------------------- */ #include "postgres_fe.h" #include #include "bbstreamer.h" #include "common/file_perm.h" #include "common/logging.h" #include "common/string.h" typedef struct bbstreamer_plain_writer { bbstreamer base; char *pathname; FILE *file; bool should_close_file; } bbstreamer_plain_writer; typedef struct bbstreamer_extractor { bbstreamer base; char *basepath; const char *(*link_map) (const char *); void (*report_output_file) (const char *); char filename[MAXPGPATH]; FILE *file; } bbstreamer_extractor; static void bbstreamer_plain_writer_content(bbstreamer *streamer, bbstreamer_member *member, const char *data, int len, bbstreamer_archive_context context); static void bbstreamer_plain_writer_finalize(bbstreamer *streamer); static void bbstreamer_plain_writer_free(bbstreamer *streamer); const bbstreamer_ops bbstreamer_plain_writer_ops = { .content = bbstreamer_plain_writer_content, .finalize = bbstreamer_plain_writer_finalize, .free = bbstreamer_plain_writer_free }; static void bbstreamer_extractor_content(bbstreamer *streamer, bbstreamer_member *member, const char *data, int len, bbstreamer_archive_context context); static void bbstreamer_extractor_finalize(bbstreamer *streamer); static void bbstreamer_extractor_free(bbstreamer *streamer); static void extract_directory(const char *filename, mode_t mode); static void extract_link(const char *filename, const char *linktarget); static FILE *create_file_for_extract(const char *filename, mode_t mode); const bbstreamer_ops bbstreamer_extractor_ops = { .content = bbstreamer_extractor_content, .finalize = bbstreamer_extractor_finalize, .free = bbstreamer_extractor_free }; /* * Create a bbstreamer that just writes data to a file. * * The caller must specify a pathname and may specify a file. The pathname is * used for error-reporting purposes either way. If file is NULL, the pathname * also identifies the file to which the data should be written: it is opened * for writing and closed when done. If file is not NULL, the data is written * there. */ bbstreamer * bbstreamer_plain_writer_new(char *pathname, FILE *file) { bbstreamer_plain_writer *streamer; streamer = palloc0(sizeof(bbstreamer_plain_writer)); *((const bbstreamer_ops **) &streamer->base.bbs_ops) = &bbstreamer_plain_writer_ops; streamer->pathname = pstrdup(pathname); streamer->file = file; if (file == NULL) { streamer->file = fopen(pathname, "wb"); if (streamer->file == NULL) pg_fatal("could not create file \"%s\": %m", pathname); streamer->should_close_file = true; } return &streamer->base; } /* * Write archive content to file. */ static void bbstreamer_plain_writer_content(bbstreamer *streamer, bbstreamer_member *member, const char *data, int len, bbstreamer_archive_context context) { bbstreamer_plain_writer *mystreamer; mystreamer = (bbstreamer_plain_writer *) streamer; if (len == 0) return; errno = 0; if (fwrite(data, len, 1, mystreamer->file) != 1) { /* if write didn't set errno, assume problem is no disk space */ if (errno == 0) errno = ENOSPC; pg_fatal("could not write to file \"%s\": %m", mystreamer->pathname); } } /* * End-of-archive processing when writing to a plain file consists of closing * the file if we opened it, but not if the caller provided it. */ static void bbstreamer_plain_writer_finalize(bbstreamer *streamer) { bbstreamer_plain_writer *mystreamer; mystreamer = (bbstreamer_plain_writer *) streamer; if (mystreamer->should_close_file && fclose(mystreamer->file) != 0) pg_fatal("could not close file \"%s\": %m", mystreamer->pathname); mystreamer->file = NULL; mystreamer->should_close_file = false; } /* * Free memory associated with this bbstreamer. */ static void bbstreamer_plain_writer_free(bbstreamer *streamer) { bbstreamer_plain_writer *mystreamer; mystreamer = (bbstreamer_plain_writer *) streamer; Assert(!mystreamer->should_close_file); Assert(mystreamer->base.bbs_next == NULL); pfree(mystreamer->pathname); pfree(mystreamer); } /* * Create a bbstreamer that extracts an archive. * * All pathnames in the archive are interpreted relative to basepath. * * Unlike e.g. bbstreamer_plain_writer_new() we can't do anything useful here * with untyped chunks; we need typed chunks which follow the rules described * in bbstreamer.h. Assuming we have that, we don't need to worry about the * original archive format; it's enough to just look at the member information * provided and write to the corresponding file. * * 'link_map' is a function that will be applied to the target of any * symbolic link, and which should return a replacement pathname to be used * in its place. If NULL, the symbolic link target is used without * modification. * * 'report_output_file' is a function that will be called each time we open a * new output file. The pathname to that file is passed as an argument. If * NULL, the call is skipped. */ bbstreamer * bbstreamer_extractor_new(const char *basepath, const char *(*link_map) (const char *), void (*report_output_file) (const char *)) { bbstreamer_extractor *streamer; streamer = palloc0(sizeof(bbstreamer_extractor)); *((const bbstreamer_ops **) &streamer->base.bbs_ops) = &bbstreamer_extractor_ops; streamer->basepath = pstrdup(basepath); streamer->link_map = link_map; streamer->report_output_file = report_output_file; return &streamer->base; } /* * Extract archive contents to the filesystem. */ static void bbstreamer_extractor_content(bbstreamer *streamer, bbstreamer_member *member, const char *data, int len, bbstreamer_archive_context context) { bbstreamer_extractor *mystreamer = (bbstreamer_extractor *) streamer; int fnamelen; Assert(member != NULL || context == BBSTREAMER_ARCHIVE_TRAILER); Assert(context != BBSTREAMER_UNKNOWN); switch (context) { case BBSTREAMER_MEMBER_HEADER: Assert(mystreamer->file == NULL); /* Prepend basepath. */ snprintf(mystreamer->filename, sizeof(mystreamer->filename), "%s/%s", mystreamer->basepath, member->pathname); /* Remove any trailing slash. */ fnamelen = strlen(mystreamer->filename); if (mystreamer->filename[fnamelen - 1] == '/') mystreamer->filename[fnamelen - 1] = '\0'; /* Dispatch based on file type. */ if (member->is_directory) extract_directory(mystreamer->filename, member->mode); else if (member->is_link) { const char *linktarget = member->linktarget; if (mystreamer->link_map) linktarget = mystreamer->link_map(linktarget); extract_link(mystreamer->filename, linktarget); } else mystreamer->file = create_file_for_extract(mystreamer->filename, member->mode); /* Report output file change. */ if (mystreamer->report_output_file) mystreamer->report_output_file(mystreamer->filename); break; case BBSTREAMER_MEMBER_CONTENTS: if (mystreamer->file == NULL) break; errno = 0; if (len > 0 && fwrite(data, len, 1, mystreamer->file) != 1) { /* if write didn't set errno, assume problem is no disk space */ if (errno == 0) errno = ENOSPC; pg_fatal("could not write to file \"%s\": %m", mystreamer->filename); } break; case BBSTREAMER_MEMBER_TRAILER: if (mystreamer->file == NULL) break; fclose(mystreamer->file); mystreamer->file = NULL; break; case BBSTREAMER_ARCHIVE_TRAILER: break; default: /* Shouldn't happen. */ pg_fatal("unexpected state while extracting archive"); } } /* * Should we tolerate an already-existing directory? * * When streaming WAL, pg_wal (or pg_xlog for pre-9.6 clusters) will have been * created by the wal receiver process. Also, when the WAL directory location * was specified, pg_wal (or pg_xlog) has already been created as a symbolic * link before starting the actual backup. So just ignore creation failures * on related directories. * * If in-place tablespaces are used, pg_tblspc and subdirectories may already * exist when we get here. So tolerate that case, too. */ static bool should_allow_existing_directory(const char *pathname) { const char *filename = last_dir_separator(pathname) + 1; if (strcmp(filename, "pg_wal") == 0 || strcmp(filename, "pg_xlog") == 0 || strcmp(filename, "archive_status") == 0 || strcmp(filename, "summaries") == 0 || strcmp(filename, "pg_tblspc") == 0) return true; if (strspn(filename, "0123456789") == strlen(filename)) { const char *pg_tblspc = strstr(pathname, "/pg_tblspc/"); return pg_tblspc != NULL && pg_tblspc + 11 == filename; } return false; } /* * Create a directory. */ static void extract_directory(const char *filename, mode_t mode) { if (mkdir(filename, pg_dir_create_mode) != 0 && (errno != EEXIST || !should_allow_existing_directory(filename))) pg_fatal("could not create directory \"%s\": %m", filename); #ifndef WIN32 if (chmod(filename, mode)) pg_fatal("could not set permissions on directory \"%s\": %m", filename); #endif } /* * Create a symbolic link. * * It's most likely a link in pg_tblspc directory, to the location of a * tablespace. Apply any tablespace mapping given on the command line * (--tablespace-mapping). (We blindly apply the mapping without checking that * the link really is inside pg_tblspc. We don't expect there to be other * symlinks in a data directory, but if there are, you can call it an * undocumented feature that you can map them too.) */ static void extract_link(const char *filename, const char *linktarget) { if (symlink(linktarget, filename) != 0) pg_fatal("could not create symbolic link from \"%s\" to \"%s\": %m", filename, linktarget); } /* * Create a regular file. * * Return the resulting handle so we can write the content to the file. */ static FILE * create_file_for_extract(const char *filename, mode_t mode) { FILE *file; file = fopen(filename, "wb"); if (file == NULL) pg_fatal("could not create file \"%s\": %m", filename); #ifndef WIN32 if (chmod(filename, mode)) pg_fatal("could not set permissions on file \"%s\": %m", filename); #endif return file; } /* * End-of-stream processing for extracting an archive. * * There's nothing to do here but sanity checking. */ static void bbstreamer_extractor_finalize(bbstreamer *streamer) { bbstreamer_extractor *mystreamer PG_USED_FOR_ASSERTS_ONLY = (bbstreamer_extractor *) streamer; Assert(mystreamer->file == NULL); } /* * Free memory. */ static void bbstreamer_extractor_free(bbstreamer *streamer) { bbstreamer_extractor *mystreamer = (bbstreamer_extractor *) streamer; pfree(mystreamer->basepath); pfree(mystreamer); }