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

    Add support for relocating tablespaces
    
    Relocate tablespace(s) by specifying one or more -T olddir=newdir. This
    supports partial matching path matching which makes it possible to
    relocate multiple tablespaces with just one parameter.
    
    Examples:
    -T /tablespaces=backup/tablespaces
    -T /tablespaces/index=backup/tablespaces/index

diff --git a/doc/src/sgml/ref/pg_basebackup.sgml b/doc/src/sgml/ref/pg_basebackup.sgml
index c379df5..0555531 100644
--- a/doc/src/sgml/ref/pg_basebackup.sgml
+++ b/doc/src/sgml/ref/pg_basebackup.sgml
@@ -138,6 +138,18 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
+      <term><option>-T <replaceable class="parameter">olddir=newdir</replaceable></option></term>
+      <term><option>--tablespace-mapping=<replaceable class="parameter">olddir=newdir</replaceable></option></term>
+      <listitem>
+       <para>
+        Relocates the tablespace(s) in directory <replaceable>olddir</replaceable>
+        to <replaceable>newdir</replaceable>. 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-mapping</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>/opt/ts</literal> is relocated to <literal>./backup/archive</literal>
+<screen>
+<prompt>$</prompt> <userinput>pg_basebackup -D $(pwd)/backup/data -T /opt/ts:$(pwd)/backup/archive</userinput>
+</screen>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index 9d13d57..8f852b2 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -33,8 +33,27 @@
 #include "streamutil.h"
 
 
+#define atooid(x)  ((Oid) strtoul((x), NULL, 10))
+
+/* Char used to separate olddir and newdir for tablespace */
+static const char TBLSPC_SEP = '=';
+
+typedef struct TablespaceListCell
+{
+	struct TablespaceListCell *next;
+	char old_dir[MAXPGPATH];
+	char new_dir[MAXPGPATH];
+} 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 +105,74 @@ 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 *old_dir);
+static bool tablespace_list_append(char *arg);
+
+/*
+ * Split tablespace argument into old_dir and new_dir, this accounts for
+ * directory name containing a colon.
+ */
+static bool
+tablespace_list_append(char *arg)
+{
+	TablespaceListCell *cell = (TablespaceListCell *) pg_malloc0(sizeof(TablespaceListCell));
+	char		*dst = cell->old_dir;
+	const char	*dst_head = dst;
+	const char	*arg_head = arg;
+
+	cell->next = NULL;
+
+	for (; *arg; arg++)
+	{
+		/* Check for overflow */
+		if (dst - dst_head >= MAXPGPATH)
+		{
+			fprintf(stderr, _("%s: directory name too long (max %d bytes)\n"),
+					progname, MAXPGPATH);
+			exit(1);
+		}
+
+		/* Split on colon not trailing a slash */
+		if (*arg == '\\' && *(arg + 1) == TBLSPC_SEP)
+			;
+		else if (*arg != TBLSPC_SEP || (arg != arg_head && *(arg - 1) == '\\'))
+			*dst++ = *arg;
+		else if (!*cell->old_dir || *cell->new_dir)
+			return false;
+		/* Found directory separator, switch to new directory */
+		else
+		{
+			dst_head = dst = cell->new_dir;
+
+			/* Add cwd if this is a relative path */
+			if (*(arg + 1) != '/')
+			{
+				if (!getcwd(dst, MAXPGPATH))
+				{
+					fprintf(stderr, _("%s: could not identify current directory: %s\n"),
+							progname, strerror(errno));
+					exit(1);
+				}
+				dst += strlen(dst);
+				*dst++ = '/';
+			}
+		}
+	}
+
+	if (!(*cell->old_dir && *cell->new_dir))
+		return false;
+
+	if (tablespace_dirs.tail)
+		tablespace_dirs.tail->next = cell;
+	else
+		tablespace_dirs.head = cell;
+	tablespace_dirs.tail = cell;
+
+	return true;
+}
+
+
 #ifdef HAVE_LIBZ
 static const char *
 get_gz_error(gzFile gzf)
@@ -113,6 +200,8 @@ usage(void)
 	printf(_("  -F, --format=p|t       output format (plain (default), tar)\n"));
 	printf(_("  -R, --write-recovery-conf\n"
 			 "                         write recovery.conf after backup\n"));
+	printf(_("  -T, --tablespace-mapping=OLDDIR=NEWDIR\n"
+			 "                         relocate tablespace matching olddir to newdir\n"));
 	printf(_("  -x, --xlog             include required WAL files in backup (fetch mode)\n"));
 	printf(_("  -X, --xlog-method=fetch|stream\n"
 			 "                         include required WAL files with specified method\n"));
@@ -861,14 +950,59 @@ 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;
+		else if (strstr(dir, cell->old_dir) != NULL)
+			return psprintf("%s/%s",
+							cell->new_dir,
+							dir + strlen(cell->old_dir) + 1);
+
+	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 = psprintf("%s/pg_tblspc/%d", basedir, oid);
+		if (unlink(linkloc) < 0 && errno != ENOENT)
+		{
+			fprintf(stderr, _("%s: could not remove symbolic link \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+		if (symlink(new_dir, linkloc) < 0)
+		{
+			fprintf(stderr, _("%s: could not create symbolic link \"%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 +1018,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 +1599,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 +1644,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: tablespace relocation is not supported on this platform\n"),
+				progname);
+#endif
+	}
+
 	PQclear(res);
 
 	/*
@@ -1658,6 +1811,7 @@ main(int argc, char **argv)
 		{"format", required_argument, NULL, 'F'},
 		{"checkpoint", required_argument, NULL, 'c'},
 		{"write-recovery-conf", no_argument, NULL, 'R'},
+		{"tablespace-mapping", required_argument, NULL, 'T'},
 		{"xlog", no_argument, NULL, 'x'},
 		{"xlog-method", required_argument, NULL, 'X'},
 		{"gzip", no_argument, NULL, 'z'},
@@ -1697,7 +1851,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:F:RT:xX:l:zZ:d:c:h:p:U:s:wWvP",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1721,6 +1875,15 @@ main(int argc, char **argv)
 			case 'R':
 				writerecoveryconf = true;
 				break;
+			case 'T':
+				if (!tablespace_list_append(optarg))
+				{
+					fprintf(stderr,
+							_("%s: invalid tablespace mapping format \"%s\", must be \"olddir=newdir\"\n"),
+							progname, optarg);
+					exit(1);
+				}
+				break;
 			case 'x':
 				if (includewal)
 				{
