diff options
-rw-r--r-- | src/backend/access/transam/xlog.c | 3 | ||||
-rw-r--r-- | src/backend/replication/basebackup.c | 238 | ||||
-rw-r--r-- | src/bin/pg_basebackup/pg_basebackup.c | 115 | ||||
-rw-r--r-- | src/include/replication/basebackup.h | 4 |
4 files changed, 333 insertions, 27 deletions
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 6bc1a6b46d..f0ad08aa33 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -10504,7 +10504,8 @@ do_pg_start_backup(const char *backupidstr, bool fast, TimeLineID *starttli_p, ti->oid = pstrdup(de->d_name); ti->path = pstrdup(buflinkpath.data); ti->rpath = relpath ? pstrdup(relpath) : NULL; - ti->size = infotbssize ? sendTablespace(fullpath, true) : -1; + ti->size = infotbssize ? + sendTablespace(fullpath, ti->oid, true, NULL) : -1; if (tablespaces) *tablespaces = lappend(*tablespaces, ti); diff --git a/src/backend/replication/basebackup.c b/src/backend/replication/basebackup.c index 1fa4551eff..9812f2a63b 100644 --- a/src/backend/replication/basebackup.c +++ b/src/backend/replication/basebackup.c @@ -19,6 +19,7 @@ #include "access/xlog_internal.h" /* for pg_start/stop_backup */ #include "catalog/pg_type.h" #include "common/file_perm.h" +#include "common/sha2.h" #include "lib/stringinfo.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -55,16 +56,25 @@ typedef struct static int64 sendDir(const char *path, int basepathlen, bool sizeonly, - List *tablespaces, bool sendtblspclinks); + List *tablespaces, bool sendtblspclinks, + StringInfo manifest, const char *tsoid); static bool sendFile(const char *readfilename, const char *tarfilename, - struct stat *statbuf, bool missing_ok, Oid dboid); -static void sendFileWithContent(const char *filename, const char *content); + struct stat *statbuf, bool missing_ok, Oid dboid, + StringInfo manifest, const char *tsoid); +static void sendFileWithContent(const char *filename, const char *content, + StringInfo manifest); static int64 _tarWriteHeader(const char *filename, const char *linktarget, struct stat *statbuf, bool sizeonly); static int64 _tarWriteDir(const char *pathbuf, int basepathlen, struct stat *statbuf, bool sizeonly); static void send_int8_string(StringInfoData *buf, int64 intval); static void SendBackupHeader(List *tablespaces); +static void InitializeManifest(StringInfo manifest); +static void AddFileToManifest(StringInfo manifest, const char *tsoid, + const char *filename, size_t size, time_t mtime, + uint8 *shabuf); +static void SendBackupManifest(StringInfo manifest); +static char *escape_field_for_manifest(const char *s); static void base_backup_cleanup(int code, Datum arg); static void perform_base_backup(basebackup_options *opt); static void parse_basebackup_options(List *options, basebackup_options *opt); @@ -241,6 +251,7 @@ perform_base_backup(basebackup_options *opt) TimeLineID endtli; StringInfo labelfile; StringInfo tblspc_map_file = NULL; + StringInfo manifest; int datadirpathlen; List *tablespaces = NIL; @@ -250,6 +261,8 @@ perform_base_backup(basebackup_options *opt) labelfile = makeStringInfo(); tblspc_map_file = makeStringInfo(); + manifest = makeStringInfo(); + InitializeManifest(manifest); total_checksum_failures = 0; @@ -286,7 +299,10 @@ perform_base_backup(basebackup_options *opt) /* Add a node for the base directory at the end */ ti = palloc0(sizeof(tablespaceinfo)); - ti->size = opt->progress ? sendDir(".", 1, true, tablespaces, true) : -1; + if (opt->progress) + ti->size = sendDir(".", 1, true, tablespaces, true, NULL, NULL); + else + ti->size = -1; tablespaces = lappend(tablespaces, ti); /* Send tablespace header */ @@ -333,7 +349,8 @@ perform_base_backup(basebackup_options *opt) struct stat statbuf; /* In the main tar, include the backup_label first... */ - sendFileWithContent(BACKUP_LABEL_FILE, labelfile->data); + sendFileWithContent(BACKUP_LABEL_FILE, labelfile->data, + manifest); /* * Send tablespace_map file if required and then the bulk of @@ -341,11 +358,12 @@ perform_base_backup(basebackup_options *opt) */ if (tblspc_map_file && opt->sendtblspcmapfile) { - sendFileWithContent(TABLESPACE_MAP, tblspc_map_file->data); - sendDir(".", 1, false, tablespaces, false); + sendFileWithContent(TABLESPACE_MAP, tblspc_map_file->data, + manifest); + sendDir(".", 1, false, tablespaces, false, manifest, NULL); } else - sendDir(".", 1, false, tablespaces, true); + sendDir(".", 1, false, tablespaces, true, manifest, NULL); /* ... and pg_control after everything else. */ if (lstat(XLOG_CONTROL_FILE, &statbuf) != 0) @@ -353,10 +371,11 @@ perform_base_backup(basebackup_options *opt) (errcode_for_file_access(), errmsg("could not stat file \"%s\": %m", XLOG_CONTROL_FILE))); - sendFile(XLOG_CONTROL_FILE, XLOG_CONTROL_FILE, &statbuf, false, InvalidOid); + sendFile(XLOG_CONTROL_FILE, XLOG_CONTROL_FILE, &statbuf, + false, InvalidOid, manifest, NULL); } else - sendTablespace(ti->path, false); + sendTablespace(ti->path, ti->oid, false, manifest); /* * If we're including WAL, and this is the main data directory we @@ -575,7 +594,7 @@ perform_base_backup(basebackup_options *opt) * complete segment. */ StatusFilePath(pathbuf, walFileName, ".done"); - sendFileWithContent(pathbuf, ""); + sendFileWithContent(pathbuf, "", manifest); } /* @@ -598,16 +617,20 @@ perform_base_backup(basebackup_options *opt) (errcode_for_file_access(), errmsg("could not stat file \"%s\": %m", pathbuf))); - sendFile(pathbuf, pathbuf, &statbuf, false, InvalidOid); + sendFile(pathbuf, pathbuf, &statbuf, false, InvalidOid, manifest, + NULL); /* unconditionally mark file as archived */ StatusFilePath(pathbuf, fname, ".done"); - sendFileWithContent(pathbuf, ""); + sendFileWithContent(pathbuf, "", manifest); } /* Send CopyDone message for the last tar file */ pq_putemptymessage('c'); } + + SendBackupManifest(manifest); + SendXlogRecPtrResult(endptr, endtli); if (total_checksum_failures) @@ -860,6 +883,151 @@ SendBackupHeader(List *tablespaces) pq_puttextmessage('C', "SELECT"); } +static void +InitializeManifest(StringInfo manifest) +{ + appendStringInfoString(manifest, "PostgreSQL-Backup-Manifest-Version 1\n"); +} + +/* + * Add an entry to the backup manifest for a file. + */ +static void +AddFileToManifest(StringInfo manifest, const char *tsoid, + const char *filename, size_t size, time_t mtime, + uint8 *shabuf) +{ + char pathbuf[MAXPGPATH]; + char *escaped_filename; + static char timebuf[128]; + static char shatextbuf[PG_SHA256_DIGEST_LENGTH * 2 + 1]; + int shatextlen; + + /* + * If this file is part of a tablespace, the filename passed to this + * function will be relative to the tar file that contains it. We want + * the pathname relative to the data directory (ignoring the intermediate + * symlink traversal). + */ + if (tsoid != NULL) + { + snprintf(pathbuf, sizeof(pathbuf), "pg_tblspc/%s/%s", tsoid, filename); + filename = pathbuf; + } + + /* Escape filename, if necessary. */ + escaped_filename = escape_field_for_manifest(filename); + + /* + * Convert time to a string. Since it's not clear what time zone to use + * and since time zone definitions can change, possibly causing confusion, + * use GMT always. + */ + pg_strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S %Z", + pg_gmtime(&mtime)); + + /* Convert checksum to hexadecimal. */ + shatextlen = + hex_encode((char *) shabuf, PG_SHA256_DIGEST_LENGTH, shatextbuf); + Assert(shatextlen + 1 == sizeof(shatextbuf)); + shatextbuf[shatextlen] = '\0'; + + /* Add to manifest. */ + appendStringInfo(manifest, "File\t%s\t%zu\t%s\t%s\n", + escaped_filename == NULL ? filename : escaped_filename, + size, timebuf, shatextbuf); + + /* Avoid leaking memory. */ + if (escaped_filename != NULL) + pfree(escaped_filename); +} + +/* + * Finalize the backup manifest, and send it to the client. + */ +static void +SendBackupManifest(StringInfo manifest) +{ + pg_sha256_ctx sha256_ctx; + uint8 shabuf[PG_SHA256_DIGEST_LENGTH]; + StringInfoData protobuf; + int shastringlen; + + /* Checksum the manifest. */ + pg_sha256_init(&sha256_ctx); + pg_sha256_update(&sha256_ctx, (uint8 *) manifest->data, manifest->len); + pg_sha256_final(&sha256_ctx, shabuf); + appendStringInfoString(manifest, "Manifest-Checksum\t"); + shastringlen = PG_SHA256_DIGEST_LENGTH * 2; + enlargeStringInfo(manifest, shastringlen); + shastringlen = hex_encode((char *) shabuf, PG_SHA256_DIGEST_LENGTH, + manifest->data + manifest->len); + Assert(shastringlen == PG_SHA256_DIGEST_LENGTH * 2); + manifest->len += shastringlen; + appendStringInfoChar(manifest, '\n'); + + /* Send CopyOutResponse message */ + pq_beginmessage(&protobuf, 'H'); + pq_sendbyte(&protobuf, 0); /* overall format */ + pq_sendint16(&protobuf, 0); /* natts */ + pq_endmessage(&protobuf); + + /* Send CopyData message */ + pq_putmessage('d', manifest->data, manifest->len); + + /* And finally CopyDone message */ + pq_putemptymessage('c'); +} + +/* + * Escape a field for inclusion in a manifest. + * + * We use the following escaping rule: If a field contains \t, \r, or \n, + * the field must be surrounded by double-quotes, and any internal double + * quotes must be doubled. Otherwise, no escaping is required. + * + * The return value is a new palloc'd string with escaping added, or NULL + * if no escaping is required. + */ +static char * +escape_field_for_manifest(const char *s) +{ + bool escaping_required = false; + int escaped_length = 2; + const char *t; + char *result; + char *r; + + for (t = s; *t != '\0'; ++t) + { + if (*t == '\t' || *t == '\r' || *t == '\n') + escaping_required = true; + if (*t == '"') + ++escaped_length; + ++escaped_length; + } + + if (!escaping_required) + return NULL; + + result = palloc(escaped_length + 1); + result[0] = '"'; + result[escaped_length - 1] = '"'; + result[escaped_length] = '\0'; + r = result + 1; + + for (t = s; *t != '\0'; ++t) + { + *(r++) = *t; + if (*t == '"') + *(r++) = *t; + } + + Assert(r == &result[escaped_length - 1]); + + return result; +} + /* * Send a single resultset containing just a single * XLogRecPtr record (in text format) @@ -920,11 +1088,16 @@ SendXlogRecPtrResult(XLogRecPtr ptr, TimeLineID tli) * Inject a file with given name and content in the output tar stream. */ static void -sendFileWithContent(const char *filename, const char *content) +sendFileWithContent(const char *filename, const char *content, + StringInfo manifest) { struct stat statbuf; int pad, len; + pg_sha256_ctx sha256_ctx; + uint8 shabuf[PG_SHA256_DIGEST_LENGTH]; + + pg_sha256_init(&sha256_ctx); len = strlen(content); @@ -957,6 +1130,11 @@ sendFileWithContent(const char *filename, const char *content) MemSet(buf, 0, pad); pq_putmessage('d', buf, pad); } + + pg_sha256_update(&sha256_ctx, (uint8 *) content, len); + pg_sha256_final(&sha256_ctx, shabuf); + AddFileToManifest(manifest, NULL, filename, len, statbuf.st_mtime, + shabuf); } /* @@ -967,7 +1145,7 @@ sendFileWithContent(const char *filename, const char *content) * Only used to send auxiliary tablespaces, not PGDATA. */ int64 -sendTablespace(char *path, bool sizeonly) +sendTablespace(char *path, char *oid, bool sizeonly, StringInfo manifest) { int64 size; char pathbuf[MAXPGPATH]; @@ -1000,7 +1178,7 @@ sendTablespace(char *path, bool sizeonly) sizeonly); /* Send all the files in the tablespace version directory */ - size += sendDir(pathbuf, strlen(path), sizeonly, NIL, true); + size += sendDir(pathbuf, strlen(path), sizeonly, NIL, true, manifest, oid); return size; } @@ -1019,7 +1197,7 @@ sendTablespace(char *path, bool sizeonly) */ static int64 sendDir(const char *path, int basepathlen, bool sizeonly, List *tablespaces, - bool sendtblspclinks) + bool sendtblspclinks, StringInfo manifest, const char *tsoid) { DIR *dir; struct dirent *de; @@ -1295,7 +1473,8 @@ sendDir(const char *path, int basepathlen, bool sizeonly, List *tablespaces, skip_this_dir = true; if (!skip_this_dir) - size += sendDir(pathbuf, basepathlen, sizeonly, tablespaces, sendtblspclinks); + size += sendDir(pathbuf, basepathlen, sizeonly, tablespaces, + sendtblspclinks, manifest, tsoid); } else if (S_ISREG(statbuf.st_mode)) { @@ -1303,7 +1482,8 @@ sendDir(const char *path, int basepathlen, bool sizeonly, List *tablespaces, if (!sizeonly) sent = sendFile(pathbuf, pathbuf + basepathlen + 1, &statbuf, - true, isDbDir ? pg_atoi(lastDir + 1, sizeof(Oid), 0) : InvalidOid); + true, isDbDir ? pg_atoi(lastDir + 1, sizeof(Oid), 0) : InvalidOid, + manifest, tsoid); if (sent || sizeonly) { @@ -1366,8 +1546,9 @@ is_checksummed_file(const char *fullpath, const char *filename) * and the file did not exist. */ static bool -sendFile(const char *readfilename, const char *tarfilename, struct stat *statbuf, - bool missing_ok, Oid dboid) +sendFile(const char *readfilename, const char *tarfilename, + struct stat *statbuf, bool missing_ok, Oid dboid, + StringInfo manifest, const char *tsoid) { FILE *fp; BlockNumber blkno = 0; @@ -1384,6 +1565,10 @@ sendFile(const char *readfilename, const char *tarfilename, struct stat *statbuf int segmentno = 0; char *segmentpath; bool verify_checksum = false; + pg_sha256_ctx sha256_ctx; + uint8 shabuf[PG_SHA256_DIGEST_LENGTH]; + + pg_sha256_init(&sha256_ctx); fp = AllocateFile(readfilename, "rb"); if (fp == NULL) @@ -1553,6 +1738,9 @@ sendFile(const char *readfilename, const char *tarfilename, struct stat *statbuf ereport(ERROR, (errmsg("base backup could not send data, aborting backup"))); + /* Also feed it to the checksum machinery. */ + pg_sha256_update(&sha256_ctx, (uint8 *) buf, cnt); + len += cnt; throttle(cnt); @@ -1577,6 +1765,7 @@ sendFile(const char *readfilename, const char *tarfilename, struct stat *statbuf { cnt = Min(sizeof(buf), statbuf->st_size - len); pq_putmessage('d', buf, cnt); + pg_sha256_update(&sha256_ctx, (uint8 *) buf, cnt); len += cnt; throttle(cnt); } @@ -1584,7 +1773,8 @@ sendFile(const char *readfilename, const char *tarfilename, struct stat *statbuf /* * Pad to 512 byte boundary, per tar format requirements. (This small - * piece of data is probably not worth throttling.) + * piece of data is probably not worth throttling, and is not checksummed + * because it's not actually part of the file.) */ pad = ((len + 511) & ~511) - len; if (pad > 0) @@ -1608,6 +1798,10 @@ sendFile(const char *readfilename, const char *tarfilename, struct stat *statbuf total_checksum_failures += checksum_failures; + pg_sha256_final(&sha256_ctx, shabuf); + AddFileToManifest(manifest, tsoid, tarfilename, statbuf->st_size, + statbuf->st_mtime, shabuf); + return true; } diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c index 44ac6e5c2f..de42a662b1 100644 --- a/src/bin/pg_basebackup/pg_basebackup.c +++ b/src/bin/pg_basebackup/pg_basebackup.c @@ -88,6 +88,12 @@ typedef struct UnpackTarState FILE *file; } UnpackTarState; +typedef struct WriteManifestState +{ + char filename[MAXPGPATH]; + FILE *file; +} WriteManifestState; + typedef void (*WriteDataCallback) (size_t nbytes, char *buf, void *callback_data); @@ -180,6 +186,12 @@ static void ReceiveTarCopyChunk(size_t r, char *copybuf, void *callback_data); static void ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum); static void ReceiveTarAndUnpackCopyChunk(size_t r, char *copybuf, void *callback_data); +static void ReceiveBackupManifest(PGconn *conn); +static void ReceiveBackupManifestChunk(size_t r, char *copybuf, + void *callback_data); +static void ReceiveBackupManifestInMemory(PGconn *conn, PQExpBuffer buf); +static void ReceiveBackupManifestInMemoryChunk(size_t r, char *copybuf, + void *callback_data); static void BaseBackup(void); static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline, @@ -924,8 +936,8 @@ ReceiveCopyData(PGconn *conn, WriteDataCallback callback, res = PQgetResult(conn); if (PQresultStatus(res) != PGRES_COPY_OUT) { - pg_log_error("could not get COPY data stream: %s", - PQerrorMessage(conn)); + pg_log_error("could not get COPY data stream: %s [%s]", + PQerrorMessage(conn), PQresStatus(PQresultStatus(res))); exit(1); } PQclear(res); @@ -1170,6 +1182,31 @@ ReceiveTarFile(PGconn *conn, PGresult *res, int rownum) } } + /* + * Normally, we emit the backup manifest as a separate file, but when + * we're writing a tarfile to stdout, we don't have that option, so + * include it in the one tarfile we've got. + */ + if (strcmp(basedir, "-") == 0) + { + char header[512]; + PQExpBufferData buf; + + initPQExpBuffer(&buf); + ReceiveBackupManifestInMemory(conn, &buf); + if (PQExpBufferBroken(&buf)) + { + pg_log_error("out of memory"); + exit(1); + } + tarCreateHeader(header, "backup_manifest", NULL, buf.len, + pg_file_create_mode, 04000, 02000, + time(NULL)); + writeTarData(&state, header, sizeof(header)); + writeTarData(&state, buf.data, buf.len); + termPQExpBuffer(&buf); + } + /* 2 * 512 bytes empty data at end of file */ writeTarData(&state, zerobuf, sizeof(zerobuf)); @@ -1641,6 +1678,63 @@ ReceiveTarAndUnpackCopyChunk(size_t r, char *copybuf, void *callback_data) } /* continuing data in existing file */ } +/* + * Receive the backup manifest file and write it out to a file. + */ +static void +ReceiveBackupManifest(PGconn *conn) +{ + WriteManifestState state; + + snprintf(state.filename, sizeof(state.filename), + "%s/backup_manifest", basedir); + state.file = fopen(state.filename, "wb"); + if (state.file == NULL) + { + pg_log_error("could not create file \"%s\": %m", state.filename); + exit(1); + } + + ReceiveCopyData(conn, ReceiveBackupManifestChunk, &state); + + fclose(state.file); +} + +/* + * Receive one chunk of the backup manifest file and write it out to a file. + */ +static void +ReceiveBackupManifestChunk(size_t r, char *copybuf, void *callback_data) +{ + WriteManifestState *state = callback_data; + + if (fwrite(copybuf, r, 1, state->file) != 1) + { + pg_log_error("could not write to file \"%s\": %m", state->filename); + exit(1); + } +} + +/* + * Receive the backup manifest file and write it out to a file. + */ +static void +ReceiveBackupManifestInMemory(PGconn *conn, PQExpBuffer buf) +{ + ReceiveCopyData(conn, ReceiveBackupManifestInMemoryChunk, buf); +} + +/* + * Receive one chunk of the backup manifest file and write it out to a file. + */ +static void +ReceiveBackupManifestInMemoryChunk(size_t r, char *copybuf, + void *callback_data) +{ + PQExpBuffer buf = callback_data; + + appendPQExpBuffer(buf, copybuf, r); +} static void BaseBackup(void) @@ -1659,6 +1753,7 @@ BaseBackup(void) maxServerMajor; int serverVersion, serverMajor; + int writing_to_stdout; Assert(conn != NULL); @@ -1822,7 +1917,8 @@ BaseBackup(void) /* * When writing to stdout, require a single tablespace */ - if (format == 't' && strcmp(basedir, "-") == 0 && PQntuples(res) > 1) + writing_to_stdout = format == 't' && strcmp(basedir, "-") == 0; + if (writing_to_stdout && PQntuples(res) > 1) { pg_log_error("can only write single tablespace to stdout, database has %d", PQntuples(res)); @@ -1851,6 +1947,19 @@ BaseBackup(void) ReceiveAndUnpackTarFile(conn, res, i); } /* Loop over all tablespaces */ + /* + * Now receive backup manifest, if appropriate. + * + * If we're writing a tarfile to stdout, ReceiveTarFile will have already + * processed the backup manifest and included it in the output tarfile. + * Such a configuration doesn't allow for writing multiple files. + * + * If we're talking to an older server, it won't send a backup manifest, + * so don't try to receive one. + */ + if (!writing_to_stdout && serverMajor >= 1300) + ReceiveBackupManifest(conn); + if (showprogress) { progress_report(PQntuples(res), NULL, true); diff --git a/src/include/replication/basebackup.h b/src/include/replication/basebackup.h index 503a5b9f0b..8fe0136fce 100644 --- a/src/include/replication/basebackup.h +++ b/src/include/replication/basebackup.h @@ -12,6 +12,7 @@ #ifndef _BASEBACKUP_H #define _BASEBACKUP_H +#include "lib/stringinfo.h" #include "nodes/replnodes.h" /* @@ -31,6 +32,7 @@ typedef struct extern void SendBaseBackup(BaseBackupCmd *cmd); -extern int64 sendTablespace(char *path, bool sizeonly); +extern int64 sendTablespace(char *path, char *oid, bool sizeonly, + StringInfo manifest); #endif /* _BASEBACKUP_H */ |