commit 8d649148205cf16cee008b4a118654af86b67a0a
Author: Steeve Lennmark <steevel@handeldsbanken.se>
Date:   Thu Jan 9 20:45:26 2014 +0000

    Add support for relocating tablespaces
    
    This is done by mapping old and new tablespace location using the format
    old_dir:new_dir.

diff --git doc/src/sgml/ref/pg_basebackup.sgml doc/src/sgml/ref/pg_basebackup.sgml
index c379df5..cad57ba 100644
--- doc/src/sgml/ref/pg_basebackup.sgml
+++ doc/src/sgml/ref/pg_basebackup.sgml
@@ -138,6 +138,18 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
+      <term><option>-T <replaceable class="parameter">oid:tablespacedir</replaceable></option></term>
+      <term><option>--tablespace=<replaceable class="parameter">oid:tablespacedir</replaceable></option></term>
+      <listitem>
+       <para>
+        Specifies the location where tablespace with <replaceable>oid</replaceable>
+        is written, <replaceable>tablespacedir</replaceable> must be an absolute path.
+        This options can be specified multiple times for multiple tablespaces.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
       <term><option>-F <replaceable class="parameter">format</replaceable></option></term>
       <term><option>--format=<replaceable class="parameter">format</replaceable></option></term>
       <listitem>
@@ -530,7 +542,7 @@ PostgreSQL documentation
   <para>
    The way <productname>PostgreSQL</productname> manages tablespaces, the path
    for all additional tablespaces must be identical whenever a backup is
-   restored. The main data directory, however, is relocatable to any location.
+   restored, if <replaceable>--tablespace</replaceable> isn't specified.
   </para>
 
   <para>
@@ -570,6 +582,14 @@ PostgreSQL documentation
    (This command will fail if there are multiple tablespaces in the
    database.)
   </para>
+
+  <para>
+   To create a backup of a two-tablespace local database where tablespace
+   <literal>archive</literal> is written to <literal>./backup/archive</literal>
+<screen>
+<prompt>$</prompt> <userinput>pg_basebackup -D $(pwd)/backup/data -T archive:$(pwd)/backup/archive</userinput>
+</screen>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git src/bin/pg_basebackup/pg_basebackup.c src/bin/pg_basebackup/pg_basebackup.c
index 9d13d57..498ba78 100644
--- src/bin/pg_basebackup/pg_basebackup.c
+++ src/bin/pg_basebackup/pg_basebackup.c
@@ -33,8 +33,24 @@
 #include "streamutil.h"
 
 
+#define atooid(x)  ((Oid) strtoul((x), NULL, 10))
+
+typedef struct TablespaceListCell
+{
+	struct TablespaceListCell *next;
+	char  *old_dir;
+	char  *new_dir;
+} TablespaceListCell;
+
+typedef struct TablespaceList
+{
+	TablespaceListCell *head;
+	TablespaceListCell *tail;
+} TablespaceList;
+
 /* Global options */
 static char *basedir = NULL;
+static TablespaceList tablespace_dirs = {NULL, NULL};
 static char *xlog_dir = "";
 static char	format = 'p';		/* p(lain)/t(ar) */
 static char *label = "pg_basebackup base backup";
@@ -86,6 +102,37 @@ static void BaseBackup(void);
 static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline,
 					 bool segment_finished);
 
+static const char *get_tablespace_dir(const char *dir);
+static void update_tablespace_symlink(Oid oid, const char *new_dir);
+static bool tablespace_list_append(TablespaceList *list, char *tablespace);
+
+static bool
+tablespace_list_append(TablespaceList *list, char *tablespace)
+{
+	TablespaceListCell *cell;
+	char *token;
+
+	if (!(token = strtok(tablespace, ":")))
+		return false;
+
+	cell = (TablespaceListCell *) pg_malloc(sizeof(TablespaceListCell));
+	cell->next = NULL;
+	cell->old_dir = pg_strdup(token);
+
+	if (!(token = strtok(NULL, ":")) || token[0] != '/')
+		return false;
+	cell->new_dir = pg_strdup(token);
+
+	if (list->tail)
+		list->tail->next = cell;
+	else
+		list->head = cell;
+	list->tail = cell;
+
+	return true;
+}
+
+
 #ifdef HAVE_LIBZ
 static const char *
 get_gz_error(gzFile gzf)
@@ -110,6 +157,8 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions controlling the output:\n"));
 	printf(_("  -D, --pgdata=DIRECTORY receive base backup into directory\n"));
+	printf(_("  -T, --tablespace=OLDDIR:NEWDIR\n"
+			 "                         relocate tablespace olddir to newdir\n"));
 	printf(_("  -F, --format=p|t       output format (plain (default), tar)\n"));
 	printf(_("  -R, --write-recovery-conf\n"
 			 "                         write recovery.conf after backup\n"));
@@ -861,14 +910,56 @@ ReceiveTarFile(PGconn *conn, PGresult *res, int rownum)
 }
 
 /*
+ * Retrieve tablespace path, either relocated or original depending on
+ * whether -T old_dir:new_dir was passed or not.
+ */
+static const char *
+get_tablespace_dir(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;
+}
+
+/*
+ * Update symlinks to reflect relocated tablespace, only applied if
+ * tablespace isn't in its original location.
+ */
+static void
+update_tablespace_symlink(Oid oid, const char *old_dir)
+{
+	const char *new_dir = get_tablespace_dir(old_dir);
+	if (strcmp(old_dir, new_dir) != 0)
+	{
+		char linkloc[MAXPGPATH];
+		snprintf(linkloc, sizeof(linkloc), "%s/pg_tblspc/%d", basedir, oid);
+		if (unlink(linkloc) < 0 && errno != ENOENT)
+		{
+			fprintf(stderr, _("%s: unable to remove \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+		if (symlink(new_dir, linkloc) < 0)
+		{
+			fprintf(stderr, _("%s: unable to create symlink \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+	}
+}
+
+/*
  * Receive a tar format stream from the connection to the server, and unpack
  * the contents of it into a directory. Only files, directories and
  * symlinks are supported, no other kinds of special files.
  *
  * If the data is for the main data directory, it will be restored in the
  * specified directory. If it's for another tablespace, it will be restored
- * in the original directory, since relocation of tablespaces is not
- * supported.
+ * in the original directory, if tablespace relocation is not enabled.
  */
 static void
 ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
@@ -884,7 +975,7 @@ ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
 	if (basetablespace)
 		strcpy(current_path, basedir);
 	else
-		strcpy(current_path, PQgetvalue(res, rownum, 1));
+		strcpy(current_path, get_tablespace_dir(PQgetvalue(res, rownum, 1)));
 
 	/*
 	 * Get the COPY data
@@ -1465,7 +1556,10 @@ BaseBackup(void)
 		 * we do anything anyway.
 		 */
 		if (format == 'p' && !PQgetisnull(res, i, 1))
-			verify_dir_is_empty_or_create(PQgetvalue(res, i, 1));
+		{
+			char *path = (char *) get_tablespace_dir(PQgetvalue(res, i, 1));
+			verify_dir_is_empty_or_create(path);
+		}
 	}
 
 	/*
@@ -1507,6 +1601,22 @@ BaseBackup(void)
 		progress_report(PQntuples(res), NULL);
 		fprintf(stderr, "\n");	/* Need to move to next line */
 	}
+
+	if (format == 'p' && tablespace_dirs.head != NULL)
+	{
+#ifdef HAVE_SYMLINK
+		for (i = 0; i < PQntuples(res); i++)
+		{
+			Oid tblspc_oid = atooid(PQgetvalue(res, i, 0));
+			if (tblspc_oid)
+				update_tablespace_symlink(tblspc_oid, PQgetvalue(res, i, 1));
+		}
+#else
+		fprintf(stderr, _("%s: not updating pg_tblspc with new tablespace relocation\n"),
+				progname);
+#endif
+	}
+
 	PQclear(res);
 
 	/*
@@ -1655,6 +1765,7 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
+		{"tablespace", required_argument, NULL, 'T'},
 		{"format", required_argument, NULL, 'F'},
 		{"checkpoint", required_argument, NULL, 'c'},
 		{"write-recovery-conf", no_argument, NULL, 'R'},
@@ -1697,7 +1808,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "D:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
+	while ((c = getopt_long(argc, argv, "D:T:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1705,6 +1816,15 @@ main(int argc, char **argv)
 			case 'D':
 				basedir = pg_strdup(optarg);
 				break;
+			case 'T':
+				if (!tablespace_list_append(&tablespace_dirs, optarg))
+				{
+					fprintf(stderr,
+							_("%s: invalid tablespace format \"%s\", must be \"old_dir:/absolute-path\"\n"),
+							progname, optarg);
+					exit(1);
+				}
+				break;
 			case 'F':
 				if (strcmp(optarg, "p") == 0 || strcmp(optarg, "plain") == 0)
 					format = 'p';
