Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * backup_manifest.c
4 : : * code for generating and sending a backup manifest
5 : : *
6 : : * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group
7 : : *
8 : : * IDENTIFICATION
9 : : * src/backend/backup/backup_manifest.c
10 : : *
11 : : *-------------------------------------------------------------------------
12 : : */
13 : : #include "postgres.h"
14 : :
15 : : #include "access/timeline.h"
16 : : #include "access/xlog.h"
17 : : #include "backup/backup_manifest.h"
18 : : #include "backup/basebackup_sink.h"
19 : : #include "common/relpath.h"
20 : : #include "mb/pg_wchar.h"
21 : : #include "utils/builtins.h"
22 : : #include "utils/json.h"
23 : :
24 : : static void AppendStringToManifest(backup_manifest_info *manifest, const char *s);
25 : :
26 : : /*
27 : : * Does the user want a backup manifest?
28 : : *
29 : : * It's simplest to always have a manifest_info object, so that we don't need
30 : : * checks for NULL pointers in too many places. However, if the user doesn't
31 : : * want a manifest, we set manifest->buffile to NULL.
32 : : */
33 : : static inline bool
1962 rhaas@postgresql.org 34 :CBC 158663 : IsManifestEnabled(backup_manifest_info *manifest)
35 : : {
1965 36 : 158663 : return (manifest->buffile != NULL);
37 : : }
38 : :
39 : : /*
40 : : * Convenience macro for appending data to the backup manifest.
41 : : */
42 : : #define AppendToManifest(manifest, ...) \
43 : : { \
44 : : char *_manifest_s = psprintf(__VA_ARGS__); \
45 : : AppendStringToManifest(manifest, _manifest_s); \
46 : : pfree(_manifest_s); \
47 : : }
48 : :
49 : : /*
50 : : * Initialize state so that we can construct a backup manifest.
51 : : *
52 : : * NB: Although the checksum type for the data files is configurable, the
53 : : * checksum for the manifest itself always uses SHA-256. See comments in
54 : : * SendBackupManifest.
55 : : */
56 : : void
1962 57 : 164 : InitializeBackupManifest(backup_manifest_info *manifest,
58 : : backup_manifest_option want_manifest,
59 : : pg_checksum_type manifest_checksum_type)
60 : : {
1759 michael@paquier.xyz 61 : 164 : memset(manifest, 0, sizeof(backup_manifest_info));
62 : 164 : manifest->checksum_type = manifest_checksum_type;
63 : :
1965 rhaas@postgresql.org 64 [ + + ]: 164 : if (want_manifest == MANIFEST_OPTION_NO)
65 : 1 : manifest->buffile = NULL;
66 : : else
67 : : {
68 : 163 : manifest->buffile = BufFileCreateTemp(false);
1739 michael@paquier.xyz 69 : 163 : manifest->manifest_ctx = pg_cryptohash_create(PG_SHA256);
70 [ - + ]: 163 : if (pg_cryptohash_init(manifest->manifest_ctx) < 0)
1334 michael@paquier.xyz 71 [ # # ]:UBC 0 : elog(ERROR, "failed to initialize checksum of backup manifest: %s",
72 : : pg_cryptohash_error(manifest->manifest_ctx));
73 : : }
74 : :
1965 rhaas@postgresql.org 75 :CBC 164 : manifest->manifest_size = UINT64CONST(0);
76 : 164 : manifest->force_encode = (want_manifest == MANIFEST_OPTION_FORCE_ENCODE);
77 : 164 : manifest->first_file = true;
78 : 164 : manifest->still_checksumming = true;
79 : :
80 [ + + ]: 164 : if (want_manifest != MANIFEST_OPTION_NO)
81 : 163 : AppendToManifest(manifest,
82 : : "{ \"PostgreSQL-Backup-Manifest-Version\": 2,\n"
83 : : "\"System-Identifier\": " UINT64_FORMAT ",\n"
84 : : "\"Files\": [",
85 : : GetSystemIdentifier());
86 : 164 : }
87 : :
88 : : /*
89 : : * Free resources assigned to a backup manifest constructed.
90 : : */
91 : : void
1739 michael@paquier.xyz 92 : 156 : FreeBackupManifest(backup_manifest_info *manifest)
93 : : {
94 : 156 : pg_cryptohash_free(manifest->manifest_ctx);
95 : 156 : manifest->manifest_ctx = NULL;
96 : 156 : }
97 : :
98 : : /*
99 : : * Add an entry to the backup manifest for a file.
100 : : */
101 : : void
684 rhaas@postgresql.org 102 : 158345 : AddFileToBackupManifest(backup_manifest_info *manifest, Oid spcoid,
103 : : const char *pathname, size_t size, pg_time_t mtime,
104 : : pg_checksum_context *checksum_ctx)
105 : : {
106 : : char pathbuf[MAXPGPATH];
107 : : int pathlen;
108 : : StringInfoData buf;
109 : :
1965 110 [ + + ]: 158345 : if (!IsManifestEnabled(manifest))
111 : 969 : return;
112 : :
113 : : /*
114 : : * If this file is part of a tablespace, the pathname passed to this
115 : : * function will be relative to the tar file that contains it. We want the
116 : : * pathname relative to the data directory (ignoring the intermediate
117 : : * symlink traversal).
118 : : */
684 119 [ + + ]: 157376 : if (OidIsValid(spcoid))
120 : : {
368 michael@paquier.xyz 121 : 348 : snprintf(pathbuf, sizeof(pathbuf), "%s/%u/%s", PG_TBLSPC_DIR, spcoid,
122 : : pathname);
1965 rhaas@postgresql.org 123 : 348 : pathname = pathbuf;
124 : : }
125 : :
126 : : /*
127 : : * Each file's entry needs to be separated from any entry that follows by
128 : : * a comma, but there's no comma before the first one or after the last
129 : : * one. To make that work, adding a file to the manifest starts by
130 : : * terminating the most recently added line, with a comma if appropriate,
131 : : * but does not terminate the line inserted for this file.
132 : : */
133 : 157376 : initStringInfo(&buf);
134 [ + + ]: 157376 : if (manifest->first_file)
135 : : {
1787 drowley@postgresql.o 136 : 163 : appendStringInfoChar(&buf, '\n');
1965 rhaas@postgresql.org 137 : 163 : manifest->first_file = false;
138 : : }
139 : : else
140 : 157213 : appendStringInfoString(&buf, ",\n");
141 : :
142 : : /*
143 : : * Write the relative pathname to this file out to the manifest. The
144 : : * manifest is always stored in UTF-8, so we have to encode paths that are
145 : : * not valid in that encoding.
146 : : */
147 : 157376 : pathlen = strlen(pathname);
148 [ + - + - ]: 314752 : if (!manifest->force_encode &&
149 : 157376 : pg_verify_mbstr(PG_UTF8, pathname, pathlen, true))
150 : : {
151 : 157376 : appendStringInfoString(&buf, "{ \"Path\": ");
406 drowley@postgresql.o 152 : 157376 : escape_json_with_len(&buf, pathname, pathlen);
1965 rhaas@postgresql.org 153 : 157376 : appendStringInfoString(&buf, ", ");
154 : : }
155 : : else
156 : : {
1965 rhaas@postgresql.org 157 :UBC 0 : appendStringInfoString(&buf, "{ \"Encoded-Path\": \"");
1479 michael@paquier.xyz 158 : 0 : enlargeStringInfo(&buf, 2 * pathlen);
159 : 0 : buf.len += hex_encode(pathname, pathlen,
160 : 0 : &buf.data[buf.len]);
1965 rhaas@postgresql.org 161 : 0 : appendStringInfoString(&buf, "\", ");
162 : : }
163 : :
1965 rhaas@postgresql.org 164 :CBC 157376 : appendStringInfo(&buf, "\"Size\": %zu, ", size);
165 : :
166 : : /*
167 : : * Convert last modification time to a string and append it to the
168 : : * manifest. Since it's not clear what time zone to use and since time
169 : : * zone definitions can change, possibly causing confusion, use GMT
170 : : * always.
171 : : */
172 : 157376 : appendStringInfoString(&buf, "\"Last-Modified\": \"");
173 : 157376 : enlargeStringInfo(&buf, 128);
174 : 157376 : buf.len += pg_strftime(&buf.data[buf.len], 128, "%Y-%m-%d %H:%M:%S %Z",
175 : 157376 : pg_gmtime(&mtime));
1787 drowley@postgresql.o 176 : 157376 : appendStringInfoChar(&buf, '"');
177 : :
178 : : /* Add checksum information. */
1965 rhaas@postgresql.org 179 [ + + ]: 157376 : if (checksum_ctx->type != CHECKSUM_TYPE_NONE)
180 : : {
181 : : uint8 checksumbuf[PG_CHECKSUM_MAX_LENGTH];
182 : : int checksumlen;
183 : :
184 : 155439 : checksumlen = pg_checksum_final(checksum_ctx, checksumbuf);
1739 michael@paquier.xyz 185 [ - + ]: 155439 : if (checksumlen < 0)
1739 michael@paquier.xyz 186 [ # # ]:UBC 0 : elog(ERROR, "could not finalize checksum of file \"%s\"",
187 : : pathname);
188 : :
1965 rhaas@postgresql.org 189 :CBC 155439 : appendStringInfo(&buf,
190 : : ", \"Checksum-Algorithm\": \"%s\", \"Checksum\": \"",
191 : : pg_checksum_type_name(checksum_ctx->type));
1479 michael@paquier.xyz 192 : 155439 : enlargeStringInfo(&buf, 2 * checksumlen);
193 : 310878 : buf.len += hex_encode((char *) checksumbuf, checksumlen,
194 : 155439 : &buf.data[buf.len]);
1787 drowley@postgresql.o 195 : 155439 : appendStringInfoChar(&buf, '"');
196 : : }
197 : :
198 : : /* Close out the object. */
1965 rhaas@postgresql.org 199 : 157376 : appendStringInfoString(&buf, " }");
200 : :
201 : : /* OK, add it to the manifest. */
202 : 157376 : AppendStringToManifest(manifest, buf.data);
203 : :
204 : : /* Avoid leaking memory. */
205 : 157376 : pfree(buf.data);
206 : : }
207 : :
208 : : /*
209 : : * Add information about the WAL that will need to be replayed when restoring
210 : : * this backup to the manifest.
211 : : */
212 : : void
1962 213 : 159 : AddWALInfoToBackupManifest(backup_manifest_info *manifest, XLogRecPtr startptr,
214 : : TimeLineID starttli, XLogRecPtr endptr,
215 : : TimeLineID endtli)
216 : : {
217 : : List *timelines;
218 : : ListCell *lc;
1965 219 : 159 : bool first_wal_range = true;
220 : 159 : bool found_start_timeline = false;
221 : :
222 [ + + ]: 159 : if (!IsManifestEnabled(manifest))
223 : 1 : return;
224 : :
225 : : /* Terminate the list of files. */
226 : 158 : AppendStringToManifest(manifest, "\n],\n");
227 : :
228 : : /* Read the timeline history for the ending timeline. */
229 : 158 : timelines = readTimeLineHistory(endtli);
230 : :
231 : : /* Start a list of LSN ranges. */
232 : 158 : AppendStringToManifest(manifest, "\"WAL-Ranges\": [\n");
233 : :
234 [ + - + - : 158 : foreach(lc, timelines)
+ - ]
235 : : {
236 : 158 : TimeLineHistoryEntry *entry = lfirst(lc);
237 : : XLogRecPtr tl_beginptr;
238 : :
239 : : /*
240 : : * We only care about timelines that were active during the backup.
241 : : * Skip any that ended before the backup started. (Note that if
242 : : * entry->end is InvalidXLogRecPtr, it means that the timeline has not
243 : : * yet ended.)
244 : : */
245 [ - + - - ]: 158 : if (!XLogRecPtrIsInvalid(entry->end) && entry->end < startptr)
1965 rhaas@postgresql.org 246 :UBC 0 : continue;
247 : :
248 : : /*
249 : : * Because the timeline history file lists newer timelines before
250 : : * older ones, the first timeline we encounter that is new enough to
251 : : * matter ought to match the ending timeline of the backup.
252 : : */
1965 rhaas@postgresql.org 253 [ + - - + ]:CBC 158 : if (first_wal_range && endtli != entry->tli)
1965 rhaas@postgresql.org 254 [ # # ]:UBC 0 : ereport(ERROR,
255 : : errmsg("expected end timeline %u but found timeline %u",
256 : : starttli, entry->tli));
257 : :
258 : : /*
259 : : * If this timeline entry matches with the timeline on which the
260 : : * backup started, WAL needs to be checked from the start LSN of the
261 : : * backup. If this entry refers to a newer timeline, WAL needs to be
262 : : * checked since the beginning of this timeline, so use the LSN where
263 : : * the timeline began.
264 : : */
1475 michael@paquier.xyz 265 [ + - ]:CBC 158 : if (starttli == entry->tli)
266 : 158 : tl_beginptr = startptr;
267 : : else
268 : : {
1475 michael@paquier.xyz 269 :UBC 0 : tl_beginptr = entry->begin;
270 : :
271 : : /*
272 : : * If we reach a TLI that has no valid beginning LSN, there can't
273 : : * be any more timelines in the history after this point, so we'd
274 : : * better have arrived at the expected starting TLI. If not,
275 : : * something's gone horribly wrong.
276 : : */
277 [ # # ]: 0 : if (XLogRecPtrIsInvalid(entry->begin))
1965 rhaas@postgresql.org 278 [ # # ]: 0 : ereport(ERROR,
279 : : errmsg("expected start timeline %u but found timeline %u",
280 : : starttli, entry->tli));
281 : : }
282 : :
1965 rhaas@postgresql.org 283 [ + - ]:CBC 158 : AppendToManifest(manifest,
284 : : "%s{ \"Timeline\": %u, \"Start-LSN\": \"%X/%08X\", \"End-LSN\": \"%X/%08X\" }",
285 : : first_wal_range ? "" : ",\n",
286 : : entry->tli,
287 : : LSN_FORMAT_ARGS(tl_beginptr),
288 : : LSN_FORMAT_ARGS(endptr));
289 : :
290 [ + - ]: 158 : if (starttli == entry->tli)
291 : : {
292 : 158 : found_start_timeline = true;
293 : 158 : break;
294 : : }
295 : :
1965 rhaas@postgresql.org 296 :UBC 0 : endptr = entry->begin;
297 : 0 : first_wal_range = false;
298 : : }
299 : :
300 : : /*
301 : : * The last entry in the timeline history for the ending timeline should
302 : : * be the ending timeline itself. Verify that this is what we observed.
303 : : */
1965 rhaas@postgresql.org 304 [ - + ]:CBC 158 : if (!found_start_timeline)
1965 rhaas@postgresql.org 305 [ # # ]:UBC 0 : ereport(ERROR,
306 : : errmsg("start timeline %u not found in history of timeline %u",
307 : : starttli, endtli));
308 : :
309 : : /* Terminate the list of WAL ranges. */
1965 rhaas@postgresql.org 310 :CBC 158 : AppendStringToManifest(manifest, "\n],\n");
311 : : }
312 : :
313 : : /*
314 : : * Finalize the backup manifest, and send it to the client.
315 : : */
316 : : void
1213 tgl@sss.pgh.pa.us 317 : 159 : SendBackupManifest(backup_manifest_info *manifest, bbsink *sink)
318 : : {
319 : : uint8 checksumbuf[PG_SHA256_DIGEST_LENGTH];
320 : : char checksumstringbuf[PG_SHA256_DIGEST_STRING_LENGTH];
1965 rhaas@postgresql.org 321 : 159 : size_t manifest_bytes_done = 0;
322 : :
323 [ + + ]: 159 : if (!IsManifestEnabled(manifest))
324 : 1 : return;
325 : :
326 : : /*
327 : : * Append manifest checksum, so that the problems with the manifest itself
328 : : * can be detected.
329 : : *
330 : : * We always use SHA-256 for this, regardless of what algorithm is chosen
331 : : * for checksumming the files. If we ever want to make the checksum
332 : : * algorithm used for the manifest file variable, the client will need a
333 : : * way to figure out which algorithm to use as close to the beginning of
334 : : * the manifest file as possible, to avoid having to read the whole thing
335 : : * twice.
336 : : */
337 : 158 : manifest->still_checksumming = false;
1664 michael@paquier.xyz 338 [ - + ]: 158 : if (pg_cryptohash_final(manifest->manifest_ctx, checksumbuf,
339 : : sizeof(checksumbuf)) < 0)
1334 michael@paquier.xyz 340 [ # # ]:UBC 0 : elog(ERROR, "failed to finalize checksum of backup manifest: %s",
341 : : pg_cryptohash_error(manifest->manifest_ctx));
1965 rhaas@postgresql.org 342 :CBC 158 : AppendStringToManifest(manifest, "\"Manifest-Checksum\": \"");
343 : :
1479 michael@paquier.xyz 344 : 158 : hex_encode((char *) checksumbuf, sizeof checksumbuf, checksumstringbuf);
345 : 158 : checksumstringbuf[PG_SHA256_DIGEST_STRING_LENGTH - 1] = '\0';
346 : :
1965 rhaas@postgresql.org 347 : 158 : AppendStringToManifest(manifest, checksumstringbuf);
348 : 158 : AppendStringToManifest(manifest, "\"}\n");
349 : :
350 : : /*
351 : : * We've written all the data to the manifest file. Rewind the file so
352 : : * that we can read it all back.
353 : : */
892 peter@eisentraut.org 354 [ - + ]: 158 : if (BufFileSeek(manifest->buffile, 0, 0, SEEK_SET))
1965 rhaas@postgresql.org 355 [ # # ]:UBC 0 : ereport(ERROR,
356 : : (errcode_for_file_access(),
357 : : errmsg("could not rewind temporary file")));
358 : :
359 : :
360 : : /*
361 : : * Send the backup manifest.
362 : : */
1401 rhaas@postgresql.org 363 :CBC 158 : bbsink_begin_manifest(sink);
1965 364 [ + + ]: 971 : while (manifest_bytes_done < manifest->manifest_size)
365 : : {
366 : : size_t bytes_to_read;
367 : :
1401 368 : 813 : bytes_to_read = Min(sink->bbs_buffer_length,
369 : : manifest->manifest_size - manifest_bytes_done);
964 peter@eisentraut.org 370 : 813 : BufFileReadExact(manifest->buffile, sink->bbs_buffer, bytes_to_read);
1401 rhaas@postgresql.org 371 : 813 : bbsink_manifest_contents(sink, bytes_to_read);
1965 372 : 813 : manifest_bytes_done += bytes_to_read;
373 : : }
1401 374 : 158 : bbsink_end_manifest(sink);
375 : :
376 : : /* Release resources */
1965 377 : 158 : BufFileClose(manifest->buffile);
378 : : }
379 : :
380 : : /*
381 : : * Append a cstring to the manifest.
382 : : */
383 : : static void
981 peter@eisentraut.org 384 : 158645 : AppendStringToManifest(backup_manifest_info *manifest, const char *s)
385 : : {
1962 rhaas@postgresql.org 386 : 158645 : int len = strlen(s);
387 : :
388 [ - + ]: 158645 : Assert(manifest != NULL);
389 [ + + ]: 158645 : if (manifest->still_checksumming)
390 : : {
1739 michael@paquier.xyz 391 [ - + ]: 158171 : if (pg_cryptohash_update(manifest->manifest_ctx, (uint8 *) s, len) < 0)
1334 michael@paquier.xyz 392 [ # # ]:UBC 0 : elog(ERROR, "failed to update checksum of backup manifest: %s",
393 : : pg_cryptohash_error(manifest->manifest_ctx));
394 : : }
1908 tmunro@postgresql.or 395 :CBC 158645 : BufFileWrite(manifest->buffile, s, len);
1962 rhaas@postgresql.org 396 : 158645 : manifest->manifest_size += len;
397 : 158645 : }
|