All of lore.kernel.org
 help / color / mirror / Atom feed
From: Steven Davies <btrfs-list@steev.me.uk>
To: linux-btrfs <linux-btrfs@vger.kernel.org>
Subject: [PATCH RFC] btrfs-progs: receive-dump, add JSON output format
Date: Sat, 16 Jan 2021 15:56:06 +0000	[thread overview]
Message-ID: <ddaea074-c805-0d52-8336-8c91a20f659d@steev.me.uk> (raw)

I have seen a few requests from users on other forums for a way to find a diff between two 
snapshots and some scripts have been created[1] which parse a btrfs-send stream to get this 
information.

This patch adds native JSON format output support to the btrfs-receive dump command which would 
make writing such scripts much easier as they can use JSON parsing libraries rather than string 
or binary decoding.

Usage: btrfs send --no-data <subvolume> | btrfs receive --dump-json

Sample output:
[
  {"operation": "subvol", "path": "./2021-01-16T03:01:36+00:00", "uuid": 
"4f6756a2-c8e8-614b-beaa-483de9a7ed13", "transid": 47864957},
  {"operation": "chown", "path": "./2021-01-16T03:01:36+00:00/", "gid": 0, "uid": 0},
  {"operation": "chmod", "path": "./2021-01-16T03:01:36+00:00/", "mode": "755"},
  {"operation": "utimes", "path": "./2021-01-16T03:01:36+00:00/", "atime": 
"2021-01-16T03:00:30+0000", "mtime": "1970-01-01T01:00:00+0100", "ctime": 
"2021-01-13T03:00:38+0000"},
  {"operation": "mkdir", "path": "./2021-01-16T03:01:36+00:00/o257-2524-0"},
  {"operation": "rename", "path": "./2021-01-16T03:01:36+00:00/o257-2524-0", "dest": 
"./2021-01-16T03:01:36+00:00/grub"},
  {}
]

Patch is against current btrfs-progs master.

[1] https://github.com/sysnux/btrfs-snapshots-diff

Signed-off-by: Steven Davies <btrfs@steev.me.uk>
---
  cmds/receive-dump.c | 268 +++++++++++++++++++++++++++++++++++++++++-----------
  cmds/receive-dump.h |   1 +
  cmds/receive.c      |  20 +++-
  3 files changed, 234 insertions(+), 55 deletions(-)

diff --git a/cmds/receive-dump.c b/cmds/receive-dump.c
index 648d9314..2c1af22e 100644
--- a/cmds/receive-dump.c
+++ b/cmds/receive-dump.c
@@ -51,7 +51,7 @@
   * Returns the length of the escaped characters. Unprintable characters are
   * escaped as octals.
   */
-static int print_path_escaped(const char *path)
+static int print_path_escaped(const char *path, int json)
  {
  	size_t i;
  	size_t path_len = strlen(path);
@@ -61,27 +61,44 @@ static int print_path_escaped(const char *path)
  		char c = path[i];

  		len++;
-		switch (c) {
-		case '\a': putchar('\\'); putchar('a'); len++; break;
-		case '\b': putchar('\\'); putchar('b'); len++; break;
-		case '\e': putchar('\\'); putchar('e'); len++; break;
-		case '\f': putchar('\\'); putchar('f'); len++; break;
-		case '\n': putchar('\\'); putchar('n'); len++; break;
-		case '\r': putchar('\\'); putchar('r'); len++; break;
-		case '\t': putchar('\\'); putchar('t'); len++; break;
-		case '\v': putchar('\\'); putchar('v'); len++; break;
-		case ' ':  putchar('\\'); putchar(' '); len++; break;
-		case '\\': putchar('\\'); putchar('\\'); len++; break;
-		default:
-			  if (!isprint(c)) {
-				  printf("\\%c%c%c",
-						  '0' + ((c & 0300) >> 6),
-						  '0' + ((c & 070) >> 3),
-						  '0' + (c & 07));
-				  len += 3;
-			  } else {
-				  putchar(c);
-			  }
+		if (!json) {
+			switch (c) {
+			case '\a': putchar('\\'); putchar('a'); len++; break;
+			case '\b': putchar('\\'); putchar('b'); len++; break;
+			case '\e': putchar('\\'); putchar('e'); len++; break;
+			case '\f': putchar('\\'); putchar('f'); len++; break;
+			case '\n': putchar('\\'); putchar('n'); len++; break;
+			case '\r': putchar('\\'); putchar('r'); len++; break;
+			case '\t': putchar('\\'); putchar('t'); len++; break;
+			case '\v': putchar('\\'); putchar('v'); len++; break;
+			case ' ':  putchar('\\'); putchar(' '); len++; break;
+			case '\\': putchar('\\'); putchar('\\'); len++; break;
+			default:
+				  if (!isprint(c)) {
+					  printf("\\%c%c%c",
+							  '0' + ((c & 0300) >> 6),
+							  '0' + ((c & 070) >> 3),
+							  '0' + (c & 07));
+					  len += 3;
+				  } else {
+					  putchar(c);
+				  }
+			}
+		} else {
+			if (c < 0x20) {
+				printf("\\u%04x", c);
+				len += 5;
+			} else if (c == '\\') {
+				putchar('\\');
+				putchar('\\');
+				len++;
+			} else if (c == '"') {
+				putchar('\\');
+				putchar('"');
+				len++;
+			} else {
+				putchar(c);
+			}
  		}
  	}
  	return len;
@@ -109,22 +126,41 @@ static int __print_dump(int subvol, void *user, const char *path,
  		out_path = full_path;
  	}

-	/* Unified header */
-	printf("%-16s", title);
-	ret = print_path_escaped(out_path);
-	if (!fmt) {
+
+	if (!r->json) {
+		/* Unified header */
+		printf("%-16s", title);
+		ret = print_path_escaped(out_path, r->json);
+		if (!fmt) {
+			putchar('\n');
+			return 0;
+		}
+		/* Short paths are aligned to 32 chars; longer paths get a single space */
+		do {
+			putchar(' ');
+		} while (++ret < 32);
+		va_start(args, fmt);
+		/* Operation specified ones */
+		vprintf(fmt, args);
+		va_end(args);
  		putchar('\n');
-		return 0;
-	}
-	/* Short paths are aligned to 32 chars; longer paths get a single space */
-	do {
+	} else {
+		/* Unified header */
+		printf(" {\"operation\": \"%s\", \"path\": \"", title);
+		ret = print_path_escaped(out_path, r->json);
+		putchar('"');
+		if (!fmt) {
+			printf("},\n");
+			return 0;
+		}
+		putchar(',');
  		putchar(' ');
-	} while (++ret < 32);
-	va_start(args, fmt);
-	/* Operation specified ones */
-	vprintf(fmt, args);
-	va_end(args);
-	putchar('\n');
+		va_start(args, fmt);
+		/* Operation specified ones */
+		vprintf(fmt, args);
+		va_end(args);
+		printf("},\n");
+	}
  	return 0;
  }

@@ -140,11 +176,18 @@ static int print_subvol(const char *path, const u8 *uuid, u64 ctransid,
  			void *user)
  {
  	char uuid_str[BTRFS_UUID_UNPARSED_SIZE];
+	struct btrfs_dump_send_args *r = user;

  	uuid_unparse(uuid, uuid_str);

-	return PRINT_DUMP_SUBVOL(user, path, "subvol", "uuid=%s transid=%llu",
-				 uuid_str, ctransid);
+	if (!r->json) {
+		return PRINT_DUMP_SUBVOL(user, path, "subvol",
+			"uuid=%s transid=%llu", uuid_str, ctransid);
+	} else {
+		return PRINT_DUMP_SUBVOL(user, path, "subvol",
+			"\"uuid\": \"%s\", \"transid\": %llu",
+			uuid_str, ctransid);
+	}
  }

  static int print_snapshot(const char *path, const u8 *uuid, u64 ctransid,
@@ -154,14 +197,23 @@ static int print_snapshot(const char *path, const u8 *uuid, u64 ctransid,
  	char uuid_str[BTRFS_UUID_UNPARSED_SIZE];
  	char parent_uuid_str[BTRFS_UUID_UNPARSED_SIZE];
  	int ret;
+	struct btrfs_dump_send_args *r = user;

  	uuid_unparse(uuid, uuid_str);
  	uuid_unparse(parent_uuid, parent_uuid_str);

-	ret = PRINT_DUMP_SUBVOL(user, path, "snapshot",
-		"uuid=%s transid=%llu parent_uuid=%s parent_transid=%llu",
+	if (r->json) {
+		ret = PRINT_DUMP_SUBVOL(user, path, "snapshot",
+			"uuid=%s transid=%llu parent_uuid=%s parent_transid=%llu",
  				uuid_str, ctransid, parent_uuid_str,
  				parent_ctransid);
+	} else {
+		ret = PRINT_DUMP_SUBVOL(user, path, "snapshot",
+			"\"uuid\": \"%s\", \"transid\": %llu, \"parent_uuid\": \"%s\", \
+			\"parent_transid\": %llu",
+				uuid_str, ctransid, parent_uuid_str,
+				parent_ctransid);
+	}
  	return ret;
  }

@@ -177,8 +229,16 @@ static int print_mkdir(const char *path, void *user)

  static int print_mknod(const char *path, u64 mode, u64 dev, void *user)
  {
-	return PRINT_DUMP(user, path, "mknod", "mode=%llo dev=0x%llx", mode,
-			  dev);
+	struct btrfs_dump_send_args *r = user;
+
+	if (!r->json) {
+		return PRINT_DUMP(user, path, "mknod", "mode=%llo dev=0x%llx",
+			  mode, dev);
+	} else {
+		return PRINT_DUMP(user, path,
+			  "mknod", "\"mode\": \"%llo\", \"dev\": \"0x%llx\"",
+			  mode, dev);
+	}
  }

  static int print_mkfifo(const char *path, void *user)
@@ -203,12 +263,26 @@ static int print_rename(const char *from, const char *to, void *user)
  	int ret;

  	PATH_CAT_OR_RET("rename", full_to, r->full_subvol_path, to, ret);
-	return PRINT_DUMP(user, from, "rename", "dest=%s", full_to);
+	if (!r->json) {
+		ret = PRINT_DUMP(user, from, "rename", "dest=%s", full_to);
+	} else {
+		ret = PRINT_DUMP(user, from, "rename", "\"dest\": \"%s\"",
+			  full_to);
+	}
+	return ret;
  }

  static int print_link(const char *path, const char *lnk, void *user)
  {
-	return PRINT_DUMP(user, path, "link", "dest=%s", lnk);
+	struct btrfs_dump_send_args *r = user;
+	int ret;
+
+	if (!r->json) {
+		ret = PRINT_DUMP(user, path, "link", "dest=%s", lnk);
+	} else {
+		ret = PRINT_DUMP(user, path, "link", "\"dest\": \"%s\"", lnk);
+	}
+	return ret;
  }

  static int print_unlink(const char *path, void *user)
@@ -224,8 +298,18 @@ static int print_rmdir(const char *path, void *user)
  static int print_write(const char *path, const void *data, u64 offset,
  		       u64 len, void *user)
  {
-	return PRINT_DUMP(user, path, "write", "offset=%llu len=%llu",
+	struct btrfs_dump_send_args *r = user;
+	int ret;
+
+	if (!r->json) {
+		ret = PRINT_DUMP(user, path, "write", "offset=%llu len=%llu",
+			  offset, len);
+	} else {
+		ret = PRINT_DUMP(user, path,
+			  "write", "\"offset\": %llu, \"len\": %llu",
  			  offset, len);
+	}
+	return ret;
  }

  static int print_clone(const char *path, u64 offset, u64 len,
@@ -239,37 +323,93 @@ static int print_clone(const char *path, u64 offset, u64 len,

  	PATH_CAT_OR_RET("clone", full_path, r->full_subvol_path, clone_path,
  			ret);
-	return PRINT_DUMP(user, path, "clone",
+
+	if (!r->json) {
+		ret = PRINT_DUMP(user, path, "clone",
  			  "offset=%llu len=%llu from=%s clone_offset=%llu",
  			  offset, len, full_path, clone_offset);
+	} else {
+		ret = PRINT_DUMP(user, path, "clone",
+			  "\"offset\": %llu, \"len\": %llu, \"from\": \"%s\""
+			  ", \"clone_offset\": %llu",
+			  offset, len, full_path, clone_offset);
+	}
+	return ret;
  }

  static int print_set_xattr(const char *path, const char *name,
  			   const void *data, int len, void *user)
  {
-	return PRINT_DUMP(user, path, "set_xattr", "name=%s data=%.*s len=%d",
+	struct btrfs_dump_send_args *r = user;
+	int ret;
+
+	if (!r->json) {
+		ret = PRINT_DUMP(user, path,
+			  "set_xattr", "name=%s data=%.*s len=%d",
+			  name, len, (char *)data, len);
+	} else {
+		ret = PRINT_DUMP(user, path,
+			  "set_xattr", "\"name\": \"%s\""
+			  ", \"data\": \"%.*s\", \"len\": %d",
  			  name, len, (char *)data, len);
+	}
+	return ret;
  }

  static int print_remove_xattr(const char *path, const char *name, void *user)
  {
+	struct btrfs_dump_send_args *r = user;
+	int ret;

-	return PRINT_DUMP(user, path, "remove_xattr", "name=%s", name);
+	if (!r->json) {
+		ret = PRINT_DUMP(user, path, "remove_xattr", "name=%s", name);
+	} else {
+		ret = PRINT_DUMP(user, path, "remove_xattr",
+			  "\"name\": \"%s\"", name);
+	}
+	return ret;
  }

  static int print_truncate(const char *path, u64 size, void *user)
  {
-	return PRINT_DUMP(user, path, "truncate", "size=%llu", size);
+	struct btrfs_dump_send_args *r = user;
+	int ret;
+
+	if (!r->json) {
+		ret = PRINT_DUMP(user, path, "truncate", "size=%llu", size);
+	} else {
+		ret = PRINT_DUMP(user, path, "truncate", "\"size\": %llu", size);
+	}
+	return ret;
  }

  static int print_chmod(const char *path, u64 mode, void *user)
  {
-	return PRINT_DUMP(user, path, "chmod", "mode=%llo", mode);
+	struct btrfs_dump_send_args *r = user;
+	int ret;
+
+	if (!r->json) {
+		ret = PRINT_DUMP(user, path, "chmod", "mode=%llo", mode);
+	} else {
+		ret = PRINT_DUMP(user, path, "chmod", "\"mode\": \"%llo\"", mode);
+	}
+	return ret;
  }

  static int print_chown(const char *path, u64 uid, u64 gid, void *user)
  {
-	return PRINT_DUMP(user, path, "chown", "gid=%llu uid=%llu", gid, uid);
+	struct btrfs_dump_send_args *r = user;
+	int ret;
+
+	if (!r->json) {
+		ret = PRINT_DUMP(user, path, "chown", "gid=%llu uid=%llu",
+			  gid, uid);
+	} else {
+		ret = PRINT_DUMP(user, path, "chown",
+			  "\"gid\": %llu, \"uid\": %llu",
+			  gid, uid);
+	}
+	return ret;
  }

  static int sprintf_timespec(struct timespec *ts, char *dest, int max_size)
@@ -297,6 +437,8 @@ static int print_utimes(const char *path, struct timespec *at,
  			struct timespec *mt, struct timespec *ct,
  			void *user)
  {
+	struct btrfs_dump_send_args *r = user;
+	int ret;
  	char at_str[TIME_STRING_MAX];
  	char mt_str[TIME_STRING_MAX];
  	char ct_str[TIME_STRING_MAX];
@@ -305,15 +447,33 @@ static int print_utimes(const char *path, struct timespec *at,
  	    sprintf_timespec(mt, mt_str, TIME_STRING_MAX - 1) < 0 ||
  	    sprintf_timespec(ct, ct_str, TIME_STRING_MAX - 1) < 0)
  		return -EINVAL;
-	return PRINT_DUMP(user, path, "utimes", "atime=%s mtime=%s ctime=%s",
+	if (!r->json) {
+		ret = PRINT_DUMP(user, path,
+			  "utimes", "atime=%s mtime=%s ctime=%s",
+			  at_str, mt_str, ct_str);
+	} else {
+		ret = PRINT_DUMP(user, path, "utimes",
+			  "\"atime\": \"%s\", \"mtime\": \"%s\", " \
+			    "\"ctime\": \"%s\"",
  			  at_str, mt_str, ct_str);
+	}
+	return ret;
  }

  static int print_update_extent(const char *path, u64 offset, u64 len,
  			       void *user)
  {
-	return PRINT_DUMP(user, path, "update_extent", "offset=%llu len=%llu",
-			  offset, len);
+	struct btrfs_dump_send_args *r = user;
+	int ret;
+
+	if (!r->json) {
+		ret = PRINT_DUMP(user, path, "update_extent",
+			   "offset=%llu len=%llu", offset, len);
+	} else {
+		ret = PRINT_DUMP(user, path, "update_extent",
+			   "\"offset\": %llu, \"len\": %llu", offset, len);
+	}
+	return ret;
  }

  struct btrfs_send_ops btrfs_print_send_ops = {
diff --git a/cmds/receive-dump.h b/cmds/receive-dump.h
index 06a61085..d62462d0 100644
--- a/cmds/receive-dump.h
+++ b/cmds/receive-dump.h
@@ -22,6 +22,7 @@
  struct btrfs_dump_send_args {
  	char full_subvol_path[PATH_MAX];
  	char root_path[PATH_MAX];
+	int json;
  };

  extern struct btrfs_send_ops btrfs_print_send_ops;
diff --git a/cmds/receive.c b/cmds/receive.c
index 2aaba3ff..e977e9db 100644
--- a/cmds/receive.c
+++ b/cmds/receive.c
@@ -1244,6 +1244,8 @@ static const char * const cmd_receive_usage[] = {
  	"                 this file system is mounted.",
  	"--dump           dump stream metadata, one line per operation,",
  	"                 does not require the MOUNT parameter",
+	"--dump-json      dump stream metadata in JSON format,",
+	"                 does not require the MOUNT parameter",
  	"-v               deprecated, alias for global -v option",
  	HELPINFO_INSERT_GLOBALS,
  	HELPINFO_INSERT_VERBOSE,
@@ -1260,6 +1262,7 @@ static int cmd_receive(const struct cmd_struct *cmd, int argc, char **argv)
  	int receive_fd = fileno(stdin);
  	u64 max_errors = 1;
  	int dump = 0;
+	int dump_json = 0;
  	int ret = 0;

  	memset(&rctx, 0, sizeof(rctx));
@@ -1285,11 +1288,13 @@ static int cmd_receive(const struct cmd_struct *cmd, int argc, char **argv)
  	optind = 0;
  	while (1) {
  		int c;
-		enum { GETOPT_VAL_DUMP = 257 };
+		enum { GETOPT_VAL_DUMP = 257,
+			GETOPT_VAL_DUMP_JSON = 258 };
  		static const struct option long_opts[] = {
  			{ "max-errors", required_argument, NULL, 'E' },
  			{ "chroot", no_argument, NULL, 'C' },
  			{ "dump", no_argument, NULL, GETOPT_VAL_DUMP },
+			{ "dump-json", no_argument, NULL, GETOPT_VAL_DUMP_JSON },
  			{ "quiet", no_argument, NULL, 'q' },
  			{ NULL, 0, NULL, 0 }
  		};
@@ -1333,6 +1338,10 @@ static int cmd_receive(const struct cmd_struct *cmd, int argc, char **argv)
  		case GETOPT_VAL_DUMP:
  			dump = 1;
  			break;
+		case GETOPT_VAL_DUMP_JSON:
+			dump = 1;
+			dump_json = 1;
+			break;
  		default:
  			usage_unknown_option(cmd, argv);
  		}
@@ -1360,12 +1369,21 @@ static int cmd_receive(const struct cmd_struct *cmd, int argc, char **argv)
  		dump_args.root_path[1] = '\0';
  		dump_args.full_subvol_path[0] = '.';
  		dump_args.full_subvol_path[1] = '\0';
+		dump_args.json = dump_json;
+		if (dump_json) {
+			putchar('[');
+			putchar('\n');
+		}
  		ret = btrfs_read_and_process_send_stream(receive_fd,
  			&btrfs_print_send_ops, &dump_args, 0, max_errors);
  		if (ret < 0) {
  			errno = -ret;
  			error("failed to dump the send stream: %m");
  		}
+		if (dump_json) {
+			//Add an empty record so there isn't a trailing ,
+			printf(" {}\n]");
+		}
  	} else {
  		ret = do_receive(&rctx, tomnt, realmnt, receive_fd, max_errors);
  	}

             reply	other threads:[~2021-01-16 17:16 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-01-16 15:56 Steven Davies [this message]
2021-01-17  7:14 ` [PATCH RFC] btrfs-progs: receive-dump, add JSON output format Sheng Mao

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=ddaea074-c805-0d52-8336-8c91a20f659d@steev.me.uk \
    --to=btrfs-list@steev.me.uk \
    --cc=linux-btrfs@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.