shithub: s3

Download patch

ref: f7e50dd002ab7060dc23d3431de85429520ca6d8
parent: 48df9a55a8a81f67d92934210842fecb7c9500ba
author: Jacob Moody <moody@posixcafe.org>
date: Mon Sep 1 13:03:50 EDT 2025

add list, refactor a bit

--- a/README
+++ b/README
@@ -1,7 +1,10 @@
 Basics:
 	Requires $AWS_ACCESS_KEY_ID, $AWS_SECRET_ACCESS_KEY, and $AWS_ENDPOINT_URL_S3 defined
-	Usage: s3cp source s3://<bucket>/destination
-	Usage: s3cp s3://<bucket>/source destination
+	Usage: 6.out cat s3://bucket/file
+	Usage: 6.out cp source s3://bucket/destination
+	Usage: 6.out cp s3://bucket/source <destination>
+	Usage: 6.out rm s3://bucket/path
+	Usage: 6.out ls s3://bucket/prefix
 
 Specifics/Bugs:
 	Uses webfs(4)
--- a/mkfile
+++ b/mkfile
@@ -1,7 +1,12 @@
 </$objtype/mkfile
 
 BIN=$home/bin/$objtype
-TARG=\
-	s3cp\
 
-</sys/src/cmd/mkmany
+OFILES=\
+	xml.$O\
+	s3.$O\
+
+HFILES=\
+	xml.h\
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/s3.c
@@ -1,0 +1,382 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <mp.h>
+#include <libsec.h>
+#include "xml.h"
+
+typedef struct {
+	char *endpoint;
+	char *host;
+	char *access;
+	char *secret;
+	char *bucket;
+	char *region;
+} S3;
+
+typedef struct {
+	char method[16];
+	char time[128];
+	uchar payhash[SHA2_256dlen];
+	char authhdr[512];
+	char mime[32];
+} Hreq;
+
+static void
+datetime(char *date, int ndate, char *time, int ntime)
+{
+	Tm t;
+
+	tmnow(&t, nil);
+	snprint(date, ndate, "%τ", tmfmt(&t, "YYYYMMDD"));
+	snprint(time, ntime, "%sT%τZ", date, tmfmt(&t, "hhmmss"));
+}
+
+#define hmac(data, dlen, key, klen, digest) hmac_sha2_256(data, dlen, key, klen, digest, nil)
+
+static void
+mkkey(char *key, char *date, char *region, char *service, uchar out[SHA2_256dlen])
+{
+	char buf[256];
+
+	snprint(buf, sizeof buf, "AWS4%s", key);
+	hmac((uchar*)date, strlen(date), (uchar*)buf, strlen(buf), out);
+	hmac((uchar*)region, strlen(region), out, SHA2_256dlen, (uchar*)buf);
+	hmac((uchar*)service, strlen(service), (uchar*)buf, SHA2_256dlen, out);
+	hmac((uchar*)"aws4_request", 12, out, SHA2_256dlen, out);
+}
+
+static void
+mkhreq(Hreq *hreq, S3 *s3, char *method, char *path)
+{
+	char date[64];
+	uchar key[SHA2_256dlen], sig[SHA2_256dlen];
+	char buf[512], req[512];
+	char *sgndhdr;
+
+	datetime(date, sizeof date, hreq->time, sizeof hreq->time);
+	if(strcmp(method, "PUT") == 0){
+		snprint(buf, sizeof buf, "content-type:%s\nhost:%s\nx-amz-content-sha256:%.*lH\nx-amz-date:%s\n",
+			hreq->mime, s3->host, SHA2_256dlen, hreq->payhash, hreq->time);
+		sgndhdr = "content-type;host;x-amz-content-sha256;x-amz-date";
+	} else if(strcmp(method, "GET") == 0 || strcmp(method, "DELETE")==0){
+		hreq->mime[0] = 0;
+		sha2_256(nil, 0, hreq->payhash, nil);
+		snprint(buf, sizeof buf, "host:%s\nx-amz-date:%s\n", s3->host, hreq->time);
+		sgndhdr = "host;x-amz-date";
+	} else
+		sysfatal("invalid method");
+
+	snprint(req, sizeof req, "%s\n/%s/%s\n%s\n%s\n%s\n%.*lH",
+		method, s3->bucket, path, "", buf, sgndhdr, SHA2_256dlen, hreq->payhash);
+	sha2_256((uchar*)req, strlen(req), key, nil);
+	snprint(buf, sizeof buf, "%s\n%s\n%s/%s/%s/aws4_request\n%.*lH",
+		"AWS4-HMAC-SHA256", hreq->time, date, s3->region, "s3", SHA2_256dlen, key);
+	mkkey(s3->secret, date, s3->region, "s3", key);
+	hmac((uchar*)buf, strlen(buf), key, SHA2_256dlen, sig);
+
+	snprint(hreq->authhdr, sizeof hreq->authhdr, "%s Credential=%s/%s/%s/%s/aws4_request, SignedHeaders=%s, Signature=%.*lH",
+		"AWS4-HMAC-SHA256", s3->access, date, s3->region, "s3", sgndhdr, SHA2_256dlen, sig);
+	snprint(hreq->method, sizeof hreq->method, "%s", method);
+}
+
+static void
+prep(S3 *s3, int cfd, char *path, Hreq *hreq)
+{
+	if(fprint(cfd, "url %s/%s/%s", s3->endpoint, s3->bucket, path) < 0)
+		sysfatal("url: %r");
+	if(fprint(cfd, "request %s", hreq->method) < 0)
+		sysfatal("request: %r");
+	if(fprint(cfd, "headers Authorization:%s", hreq->authhdr) < 0)
+		sysfatal("headers2: %r");
+	if(fprint(cfd, "headers x-amz-date:%s\nx-amz-content-sha256:%.*lH", hreq->time, SHA2_256dlen, hreq->payhash) < 0)
+		sysfatal("headers: %r");
+	if(hreq->mime[0] != 0 && fprint(cfd, "contenttype %s", hreq->mime) < 0)
+		sysfatal("contenttype: %r");
+}
+
+static void
+download(S3 *s3, int cfd, char *conn, char *path, Biobuf *local)
+{
+	int bfd;
+	long n;
+	char buf[64];
+	char data[8192];
+	Hreq hreq;
+
+	mkhreq(&hreq, s3, "GET", path);
+	prep(s3, cfd, path, &hreq);
+	snprint(buf, sizeof buf, "/mnt/web/%s/body", conn);
+	bfd = open(buf, OREAD);
+	if(bfd < 0)
+		sysfatal("download body: %r");
+	for(;;){
+		n = read(bfd, data, sizeof data);
+		if(n < 0)
+			sysfatal("download body: %r");
+		if(n == 0)
+			return;
+		Bwrite(local, data, n);
+	}
+}
+
+static void
+mimetype(char *path, char *out, int nout)
+{
+	int p[2];
+	char buf[256];
+	int n;
+
+	pipe(p);
+	switch(fork()){
+	case 0:
+		close(0);
+		dup(p[0], 1);
+		close(2);
+		close(p[0]);
+		close(p[1]);
+		execl("/bin/file", "file", "-m", path, nil);
+		sysfatal("execl: %r");
+	default:
+		close(p[0]);
+		n = read(p[1], buf, sizeof buf - 1);
+		if(n <= 0)
+			sysfatal("no mimetype found");
+		buf[n - 1] = 0;
+		snprint(out, nout, "%s", buf);
+	}
+}
+
+static void
+upload(S3 *s3, int cfd, char *conn, char *localpath, char *remotepath)
+{
+	DigestState *ds;
+	uchar data[8192];
+	long n;
+	int fd, bfd;
+	char buf[256];
+	Hreq hreq;
+
+	mimetype(localpath, hreq.mime, sizeof hreq.mime);
+	fd = open(localpath, OREAD);
+	if(fd < 0)
+		sysfatal("upload open: %r");
+	for(ds = nil;;){
+		n = read(fd, data, sizeof data);
+		if(n < 0)
+			sysfatal("file read: %r");
+		if(n == 0)
+			break;
+		ds = sha2_256(data, n, nil, ds);
+	}
+	sha2_256(nil, 0, hreq.payhash, ds);
+	seek(fd, 0, 0);
+
+	mkhreq(&hreq, s3, "PUT", remotepath);
+	prep(s3, cfd, remotepath, &hreq);
+	snprint(buf, sizeof buf, "/mnt/web/%s/postbody", conn);
+	bfd = open(buf, OWRITE);
+	if(bfd < 0)
+		sysfatal("upload postbody open: %r");
+	for(;;){
+		n = read(fd, buf, sizeof buf);
+		if(n < 0)
+			sysfatal("file read: %r");
+		if(n == 0)
+			break;
+		if(write(bfd, buf, n) < 0)
+			sysfatal("upload write: %r");
+	}
+	close(bfd);
+
+	snprint(buf, sizeof buf, "/mnt/web/%s/body", conn);
+	if((bfd = open(buf, OREAD)) < 0)
+		sysfatal("open3: %r");
+	close(bfd);
+}
+
+static int
+parseuri(S3 *s3, char *path, int npath, char *arg)
+{
+	char *p;
+
+	if(strstr(arg, "s3://") != arg)
+		return -1;
+	arg+=5;
+	p = strchr(arg, '/');
+	if(p == nil || p == arg)
+		return -1;
+	snprint(path, npath, "%s", p+1);
+	s3->bucket = strdup(arg);
+	s3->bucket[p-arg] = 0;
+	return 0;
+}
+
+_Noreturn static void
+usage(void)
+{
+	fprint(2, "Requires $AWS_ACCESS_KEY_ID, $AWS_SECRET_ACCESS_KEY, and $AWS_ENDPOINT_URL_S3 defined\n");
+	fprint(2, "Usage: %s cat s3://bucket/file\n", argv0);
+	fprint(2, "Usage: %s cp source s3://bucket/destination\n", argv0);
+	fprint(2, "Usage: %s cp s3://bucket/source <destination>\n", argv0);
+	fprint(2, "Usage: %s rm s3://bucket/path\n", argv0);
+	fprint(2, "Usage: %s ls s3://bucket/prefix\n", argv0);
+	exits("usage");
+}
+
+static void
+cp(S3 *s3, int cfd, char *conn, int argc, char **argv)
+{
+	char path[512];
+	Biobuf *b;
+	int fd;
+
+	if(argc == 0 || argc > 2)
+		usage();
+	if(parseuri(s3, path, sizeof path, argv[0]) == 0){
+		if(argc > 1 && parseuri(s3, path, sizeof path, argv[1]) == 0)
+			sysfatal("s3:// → s3:// is not implemented");
+		if(argc == 1)
+			fd = 1;
+		else {
+			fd = create(argv[1], OWRITE, 0644);
+			if(fd < 0)
+				sysfatal("create: %r");
+		}
+		b = Bfdopen(fd, OWRITE);
+		if(b == nil)
+			sysfatal("Bfdopen: %r");
+		download(s3, cfd, conn, path, b);
+		return;
+	}
+	if(argc == 1 || parseuri(s3, path, sizeof path, argv[1]) < 0)
+		usage();
+	upload(s3, cfd, conn, argv[0], path);
+}
+
+static void
+delete(S3 *s3, int cfd, char *conn, int argc, char **argv)
+{
+	int fd;
+	char buf[256];
+	Hreq hreq;
+	char path[512];
+
+	if(argc == 0)
+		usage();
+	if(parseuri(s3, path, sizeof path, argv[0]) < 0)
+		usage();
+	mkhreq(&hreq, s3, "DELETE", path);
+	prep(s3, cfd, path, &hreq);
+	snprint(buf, sizeof buf, "/mnt/web/%s/body", conn);
+	fd = open(buf, OREAD);
+	if(fd < 0)
+		sysfatal("delete failed: %r");
+	close(fd);
+}
+
+static void
+list(S3 *s3, int cfd, char *conn, int argc, char **argv)
+{
+	int p[2];
+	Biobuf *in, *out;
+	Xelem *x;
+	char path[512];
+
+	if(argc == 0)
+		usage();
+	if(parseuri(s3, path, sizeof path, argv[0]) < 0){
+		fprint(2, "parseuri failed\n");
+		usage();
+	}
+	if(pipe(p) < 0)
+		sysfatal("pipe: %r");
+	switch(fork()){
+	case -1:
+		sysfatal("fork: %r");
+	case 0:
+		close(p[1]);
+		in = Bfdopen(p[0], OWRITE);
+		if(in == nil)
+			sysfatal("Bfdopen: %r");
+		download(s3, cfd, conn, path, in);
+		exits(nil);
+	default:
+		close(p[0]);
+		break;
+	}
+	out = Bfdopen(p[1], OREAD);
+	if(out == nil)
+		sysfatal("Bfdopen: %r");
+	x = xmlread(out, 0);
+	if(x == nil)
+		sysfatal("file was not valid XML, maybe not a prefix?");
+	if((x = xmlget(x, "Contents", nil)) == nil)
+		sysfatal("xml did not have Contents field");
+
+	for(; x != nil && xmlget(x, "Key", nil) != nil; x = x->next){
+		print("%s\n", xmlget(x, "Key", nil)->v);
+	}
+}
+
+struct {
+	char *cmd;
+	void (*fn)(S3*,int,char*,int,char**);
+} cmdtab[] = {
+	{ "cp", cp },
+	{ "cat", cp },
+	{ "rm", delete },
+	{ "ls", list },
+};
+
+void
+main(int argc , char **argv)
+{
+	S3 s3;
+	int fd;
+	int i;
+	long n;
+	char buf[64];
+
+	tmfmtinstall();
+	fmtinstall('H', encodefmt);
+	ARGBEGIN{
+	default:
+		usage();
+		break;
+	}ARGEND
+	if(argc == 0)
+		usage();
+
+	s3.access = getenv("AWS_ACCESS_KEY_ID");
+	s3.secret = getenv("AWS_SECRET_ACCESS_KEY");
+	s3.endpoint = getenv("AWS_ENDPOINT_URL_S3");
+	s3.region = getenv("AWS_DEFAULT_REGION");
+	if(s3.access == nil || s3.secret == nil || s3.endpoint == nil)
+		usage();
+	if(s3.region == nil)
+		s3.region = "auto";
+
+	s3.host = strstr(s3.endpoint, "://");
+	if(s3.host == nil)
+		sysfatal("invalid endpoint url");
+	s3.host += 3;
+
+	fd = open("/mnt/web/clone", ORDWR);
+	if(fd < 0)
+		sysfatal("open: %r");
+	n = read(fd, buf, sizeof buf - 1);
+	if(n <= 0)
+		sysfatal("read: %r");
+	buf[n-1] = 0;
+
+	for(i = 0; i < nelem(cmdtab); i++){
+		if(strcmp(argv[0], cmdtab[i].cmd) != 0)
+			continue;
+		argv++;
+		argc--;
+		cmdtab[i].fn(&s3, fd, buf, argc, argv);
+		exits(nil);
+	}
+	sysfatal("unsupported cmd: %s", argv[0]);
+}
--- a/s3cp.c
+++ /dev/null
@@ -1,299 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <mp.h>
-#include <libsec.h>
-
-typedef struct {
-	char *endpoint;
-	char *host;
-	char *access;
-	char *secret;
-	char *bucket;
-	char *region;
-} S3;
-
-typedef struct {
-	char method[16];
-	char time[128];
-	uchar payhash[SHA2_256dlen];
-	char authhdr[512];
-	char mime[32];
-} Hreq;
-
-static void
-datetime(char *date, int ndate, char *time, int ntime)
-{
-	Tm t;
-
-	tmnow(&t, nil);
-	snprint(date, ndate, "%τ", tmfmt(&t, "YYYYMMDD"));
-	snprint(time, ntime, "%sT%τZ", date, tmfmt(&t, "hhmmss"));
-}
-
-#define hmac(data, dlen, key, klen, digest) hmac_sha2_256(data, dlen, key, klen, digest, nil)
-
-static void
-mkkey(char *key, char *date, char *region, char *service, uchar out[SHA2_256dlen])
-{
-	char buf[256];
-
-	snprint(buf, sizeof buf, "AWS4%s", key);
-	hmac((uchar*)date, strlen(date), (uchar*)buf, strlen(buf), out);
-	hmac((uchar*)region, strlen(region), out, SHA2_256dlen, (uchar*)buf);
-	hmac((uchar*)service, strlen(service), (uchar*)buf, SHA2_256dlen, out);
-	hmac((uchar*)"aws4_request", 12, out, SHA2_256dlen, out);
-}
-
-static void
-mkhreq(Hreq *hreq, S3 *s3, char *method, char *path)
-{
-	char date[64];
-	uchar key[SHA2_256dlen], sig[SHA2_256dlen];
-	char buf[512], req[512];
-	char *sgndhdr;
-
-	datetime(date, sizeof date, hreq->time, sizeof hreq->time);
-	if(strcmp(method, "PUT") == 0){
-		snprint(buf, sizeof buf, "content-type:%s\nhost:%s\nx-amz-content-sha256:%.*lH\nx-amz-date:%s\n",
-			hreq->mime, s3->host, SHA2_256dlen, hreq->payhash, hreq->time);
-		sgndhdr = "content-type;host;x-amz-content-sha256;x-amz-date";
-	} else if(strcmp(method, "GET") == 0 || strcmp(method, "DELETE")==0){
-		hreq->mime[0] = 0;
-		sha2_256(nil, 0, hreq->payhash, nil);
-		snprint(buf, sizeof buf, "host:%s\nx-amz-date:%s\n", s3->host, hreq->time);
-		sgndhdr = "host;x-amz-date";
-	} else
-		sysfatal("invalid method");
-
-	snprint(req, sizeof req, "%s\n/%s/%s\n%s\n%s\n%s\n%.*lH",
-		method, s3->bucket, path, "", buf, sgndhdr, SHA2_256dlen, hreq->payhash);
-	sha2_256((uchar*)req, strlen(req), key, nil);
-	snprint(buf, sizeof buf, "%s\n%s\n%s/%s/%s/aws4_request\n%.*lH",
-		"AWS4-HMAC-SHA256", hreq->time, date, s3->region, "s3", SHA2_256dlen, key);
-	mkkey(s3->secret, date, s3->region, "s3", key);
-	hmac((uchar*)buf, strlen(buf), key, SHA2_256dlen, sig);
-
-	snprint(hreq->authhdr, sizeof hreq->authhdr, "%s Credential=%s/%s/%s/%s/aws4_request, SignedHeaders=%s, Signature=%.*lH",
-		"AWS4-HMAC-SHA256", s3->access, date, s3->region, "s3", sgndhdr, SHA2_256dlen, sig);
-	snprint(hreq->method, sizeof hreq->method, "%s", method);
-}
-
-static void
-prep(S3 *s3, int cfd, char *path, Hreq *hreq)
-{
-	if(fprint(cfd, "url %s/%s/%s", s3->endpoint, s3->bucket, path) < 0)
-		sysfatal("url: %r");
-	if(fprint(cfd, "request %s", hreq->method) < 0)
-		sysfatal("request: %r");
-	if(fprint(cfd, "headers Authorization:%s", hreq->authhdr) < 0)
-		sysfatal("headers2: %r");
-	if(fprint(cfd, "headers x-amz-date:%s\nx-amz-content-sha256:%.*lH", hreq->time, SHA2_256dlen, hreq->payhash) < 0)
-		sysfatal("headers: %r");
-	if(hreq->mime[0] != 0 && fprint(cfd, "contenttype %s", hreq->mime) < 0)
-		sysfatal("contenttype: %r");
-}
-
-static void
-download(S3 *s3, char *path, char *localpath, int cfd, char *conn)
-{
-	int fd, bfd;
-	long n;
-	char buf[64];
-	char data[8192];
-	Hreq hreq;
-
-	fd = create(localpath, OWRITE, 0644);
-	if(fd < 0)
-		sysfatal("download create: %r");
-	mkhreq(&hreq, s3, "GET", path);
-	prep(s3, cfd, path, &hreq);
-	snprint(buf, sizeof buf, "/mnt/web/%s/body", conn);
-	bfd = open(buf, OREAD);
-	if(bfd < 0)
-		sysfatal("download body: %r");
-	for(;;){
-		n = read(bfd, data, sizeof data);
-		if(n < 0)
-			sysfatal("download body: %r");
-		if(n == 0)
-			return;
-		write(fd, data, n);
-	}
-}
-
-static void
-mimetype(char *path, char *out, int nout)
-{
-	int p[2];
-	char buf[256];
-	int n;
-
-	pipe(p);
-	switch(fork()){
-	case 0:
-		close(0);
-		dup(p[0], 1);
-		close(2);
-		close(p[0]);
-		close(p[1]);
-		execl("/bin/file", "file", "-m", path, nil);
-		sysfatal("execl: %r");
-	default:
-		close(p[0]);
-		n = read(p[1], buf, sizeof buf - 1);
-		if(n <= 0)
-			sysfatal("no mimetype found");
-		buf[n - 1] = 0;
-		snprint(out, nout, "%s", buf);
-	}
-}
-
-static void
-upload(S3 *s3, char *path, char *localpath, int cfd, char *conn)
-{
-	DigestState *ds;
-	uchar data[8192];
-	long n;
-	int fd, bfd;
-	char buf[256];
-	Hreq hreq;
-
-	mimetype(localpath, hreq.mime, sizeof hreq.mime);
-	fd = open(localpath, OREAD);
-	if(fd < 0)
-		sysfatal("upload open: %r");
-	for(ds = nil;;){
-		n = read(fd, data, sizeof data);
-		if(n < 0)
-			sysfatal("file read: %r");
-		if(n == 0)
-			break;
-		ds = sha2_256(data, n, nil, ds);
-	}
-	sha2_256(nil, 0, hreq.payhash, ds);
-	seek(fd, 0, 0);
-
-	mkhreq(&hreq, s3, "PUT", path);
-	prep(s3, cfd, path, &hreq);
-	snprint(buf, sizeof buf, "/mnt/web/%s/postbody", conn);
-	bfd = open(buf, OWRITE);
-	if(bfd < 0)
-		sysfatal("upload postbody open: %r");
-	for(;;){
-		n = read(fd, buf, sizeof buf);
-		if(n < 0)
-			sysfatal("file read: %r");
-		if(n == 0)
-			break;
-		if(write(bfd, buf, n) < 0)
-			sysfatal("upload write: %r");
-	}
-	close(bfd);
-
-	snprint(buf, sizeof buf, "/mnt/web/%s/body", conn);
-	if((bfd = open(buf, OREAD)) < 0)
-		sysfatal("open3: %r");
-	close(bfd);
-}
-
-static void
-delete(S3 *s3, char *path, char*, int cfd, char *conn)
-{
-	int fd;
-	char buf[256];
-	Hreq hreq;
-
-	mkhreq(&hreq, s3, "DELETE", path);
-	prep(s3, cfd, path, &hreq);
-	snprint(buf, sizeof buf, "/mnt/web/%s/body", conn);
-	fd = open(buf, OREAD);
-	if(fd < 0)
-		sysfatal("delete failed: %r");
-	close(fd);
-}
-
-_Noreturn static void
-usage(void)
-{
-	fprint(2, "Requires $AWS_ACCESS_KEY_ID, $AWS_SECRET_ACCESS_KEY, and $AWS_ENDPOINT_URL_S3 defined\n");
-	fprint(2, "Usage: %s source s3://<bucket>/destination\n", argv0);
-	fprint(2, "Usage: %s s3://<bucket>/source destination\n", argv0);
-	exits("usage");
-}
-
-void
-main(int argc , char **argv)
-{
-	S3 s3;
-	int fd;
-	long n;
-	char buf[64];
-	char *p, *path, *localpath;
-	void (*op)(S3*,char*,char*,int,char*);
-	int rflag;
-
-	tmfmtinstall();
-	fmtinstall('H', encodefmt);
-	rflag = 0;
-	ARGBEGIN{
-	case 'r':
-		rflag++;
-		break;
-	default:
-		usage();
-		break;
-	}ARGEND
-	if(rflag && argc < 1)
-		usage();
-	else if(!rflag && argc < 2)
-		usage();
-
-	s3.access = getenv("AWS_ACCESS_KEY_ID");
-	s3.secret = getenv("AWS_SECRET_ACCESS_KEY");
-	s3.endpoint = getenv("AWS_ENDPOINT_URL_S3");
-	s3.region = getenv("AWS_DEFAULT_REGION");
-	if(s3.access == nil || s3.secret == nil || s3.endpoint == nil)
-		usage();
-	if(s3.region == nil)
-		s3.region = "auto";
-
-	s3.host = strstr(s3.endpoint, "://");
-	if(s3.host == nil)
-		sysfatal("invalid endpoint url");
-	s3.host += 3;
-	
-	if(strstr(argv[0], "s3://")==argv[0]){
-		s3.bucket = strdup(argv[0]+5);
-		if(rflag){
-			op = delete;
-			localpath = nil;
-		} else {
-			if(strstr(argv[1], "s3://")==argv[1])
-				sysfatal("s3:// → s3:// not implemented");
-			localpath = strdup(argv[1]);
-			op = download;
-		}
-	} else if(!rflag && strstr(argv[1], "s3://")==argv[1]){
-		localpath = strdup(argv[0]);
-		s3.bucket = strdup(argv[1]+5);
-		op = upload;
-	} else
-		usage();
-
-	p = strchr(s3.bucket, '/');
-	if(p == nil)
-		sysfatal("no path provided within bucket");
-	*p = 0;
-	path = p+1;
-
-	fd = open("/mnt/web/clone", ORDWR);
-	if(fd < 0)
-		sysfatal("open: %r");
-	n = read(fd, buf, sizeof buf - 1);
-	if(n <= 0)
-		sysfatal("read: %r");
-	buf[n-1] = 0;
-
-	op(&s3, path, localpath, fd, buf);
-	exits(nil);
-}
--- /dev/null
+++ b/xml.c
@@ -1,0 +1,319 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ctype.h>
+#include "xml.h"
+
+static char *escmap[] =
+{
+	"\x06\"&quot;",
+	"\x06\'&apos;",
+	"\x04<&lt;",
+	"\x04>&gt;",
+	"\x05&&amp;",
+};
+
+enum
+{
+	Xmlvalue = 2,
+};
+
+static char *
+unxml(char *orig)
+{
+	char *s, *o, *e;
+	int i, rsz;
+	Rune r;
+
+	for(s = orig, o = orig; *s != 0;){
+next:
+		if(*s == '\r'){
+			*o++ = '\n';
+			s += s[1] == '\n' ? 2 : 1;
+			continue;
+		}
+
+		rsz = chartorune(&r, s);
+
+		if(r == '&'){
+			if(s[1] == '#' && (e = strchr(s+2, ';')) != nil && e != s+2){
+				s += 2;
+				if(*s == 'x'){
+					*s = '0';
+					o += dec16((uchar*)o, e-o, s, e-s);
+				}else if(isdigit(*s)){
+					*o++ = atoi(s);
+				}
+				s = e+1;
+				continue;
+			}else{
+				for(i = 0; i < nelem(escmap); i++){
+					if(strncmp(s, &escmap[i][2], escmap[i][0]) == 0){
+						*o++ = escmap[i][1];
+						s += escmap[i][0];
+						goto next;
+					}
+				}
+			}
+		}
+
+		memmove(o, s, rsz);
+		s += rsz;
+		o += rsz;
+	}
+
+	*o = 0;
+	return orig;
+}
+
+static Xattr *
+xmlattr(char *s, int *err)
+{
+	Xattr *a, *attrs;
+	char *p;
+
+	attrs = nil;
+	*err = 0;
+
+	for(; *s;){
+		a = mallocz(sizeof(*a), 1);
+		a->n = s;
+		for(; *s && *s != '='; s++);
+		if(*s != '='){
+			werrstr("xml sucks (%d)", *s);
+			goto error;
+		}
+		*s++ = 0;
+		if(*s != '\'' && *s != '\"'){
+			werrstr("xml is complicated (%d)", *s);
+			goto error;
+		}
+		a->v = s+1;
+		s = utfrune(a->v, *s);
+		if(s == nil){
+			werrstr("xml is broken");
+			goto error;
+		}
+		*s++ = 0;
+		a->next = attrs;
+		a->n = unxml(a->n);
+		a->v = unxml(a->v);
+		attrs = a;
+		if(*s == ' ')
+			s++;
+		if((p = strchr(a->n, ':')) != nil && strncmp(p, ":zdef", 5) == 0)
+			*p = 0;
+	}
+
+	return attrs;
+error:
+	*err = 1;
+	free(a);
+	for(; attrs != nil; attrs = a){
+		a = attrs->next;
+		free(attrs);
+	}
+	return nil;
+}
+
+static Xelem *
+xmlread_(Biobufhdr *h, Xelem *par, int flags)
+{
+	char *s, *t;
+	Xelem *x, *ch;
+	int r, closed, len, err;
+
+	x = nil;
+
+	for(;;){
+		r = Bgetrune(h);
+		if(r < 0){
+			werrstr("xmlread: %r");
+			goto error;
+		}
+		if(r == '<')
+			break;
+		if(isspacerune(r))
+			continue;
+		if(flags & Xmlvalue && par != nil){
+			Bungetrune(h);
+			if((s = Brdstr(h, '<', 1)) == nil){
+				werrstr("xmlread: %r");
+				goto error;
+			}
+			par->v = unxml(s);
+			if((s = Brdstr(h, '>', 1)) == nil){
+				free(par->v);
+				par->v = nil;
+				werrstr("xmlread: %r");
+			}
+			free(s);
+			return nil;
+		}
+		werrstr("xmlread: unexpected rune (%C)", r);
+		goto error;
+	}
+
+	s = Brdstr(h, '>', 1);
+	if(s == nil){
+		werrstr("xmlread: %r");
+		goto error;
+	}
+	if(s[0] == '/'){
+		free(s);
+		return nil;
+	}
+	if(s[0] == '?'){
+		free(s);
+		return xmlread_(h, par, flags);
+	}
+
+	x = mallocz(sizeof(*x), 1);
+	x->priv = s;
+	x->n = s;
+
+	if(strncmp(x->n, "zdef", 4) == 0){
+		if((x->n = strchr(x->n, ':')) == nil){
+			werrstr("xmlread: zdef without ':'");
+			goto error;
+		}
+		x->n += 1;
+	}
+
+	len = strlen(s);
+	if(s[len-1] == '/' || s[len-1] == '?'){
+		closed = 1;
+		s[len-1] = 0;
+	}else
+		closed = flags & Xmlstartonly;
+
+	for(; *s && *s != ' '; s++);
+	if(*s){
+		*s++ = 0;
+		x->a = xmlattr(s, &err);
+		if(err != 0)
+			goto error;
+	}
+
+	if(strcmp(x->n, "html") == 0){
+		for(len = 0;; len += r){
+			s = Brdstr(h, '>', 0);
+			if(s == nil){
+				werrstr("xmlread: %r");
+				goto error;
+			}
+
+			r = strlen(s);
+			x->v = realloc(x->v, len + r + 1);
+			if(x->v == nil){
+				werrstr("xmlread: %r");
+				goto error;
+			}
+			strcpy(x->v+len, s);
+			free(s);
+			t = strstr(x->v+len, "</html>");
+			if(t != nil){
+				*t = 0;
+				return x;
+			}
+		}
+	}
+
+	if(!closed){
+		for(;;){
+			flags = Xmlvalue;
+			ch = xmlread_(h, x, flags);
+			if(ch == nil)
+				break;
+			ch->next = x->ch;
+			x->ch = ch;
+		}
+	}
+
+	return x;
+
+error:
+	xmlfree(x);
+	return nil;
+}
+
+Xelem *
+xmlread(Biobuf *b, int flags)
+{
+	return xmlread_(b, nil, flags & Xmlstartonly);
+}
+
+void
+xmlfree(Xelem *x)
+{
+	Xattr *a, *ta;
+	Xelem *n, *n2;
+
+	if(x == nil)
+		return;
+
+	xmlfree(x->ch);
+	free(x->v);
+	x->ch = nil;
+	x->v = nil;
+	free(x->priv);
+	for(a = x->a; a != nil; a = ta){
+		ta = a->next;
+		free(a);
+	}
+
+	for(n = x->next; n != nil; n = n2){
+		n2 = n->next;
+		n->next = nil;
+		xmlfree(n);
+	}
+
+	free(x);
+}
+
+Xelem *
+xmlget(Xelem *x, char *path, ...)
+{
+	char **s;
+
+	for(s = &path; *s != nil; s++){
+		for(x = x->ch; x != nil && strcmp(x->n, *s) != 0; x = x->next);
+		if(x == nil)
+			return nil;
+	}
+
+	return x;
+}
+
+Xattr *
+xmlgetattr(Xattr *a, char *name)
+{
+	for(; a != nil; a = a->next)
+		if(strcmp(a->n, name) == 0)
+			return a;
+	return nil;
+}
+
+static void
+xmlprint_(Xelem *x, int fd, int off)
+{
+	Xattr *a;
+
+	for(; x != nil; x = x->next){
+		fprint(fd, "%*c%q", off, ' ', x->n);
+		if(x->v != nil)
+			fprint(fd, "=%#q", x->v);
+		for(a = x->a; a != nil; a = a->next)
+			fprint(fd, " %q=%#q", a->n, a->v);
+		fprint(fd, "\n");
+		off += 4;
+		xmlprint_(x->ch, fd, off);
+		off -= 4;
+	}
+}
+
+void
+xmlprint(Xelem *x, int fd)
+{
+	xmlprint_(x, fd, 0);
+}
--- /dev/null
+++ b/xml.h
@@ -1,0 +1,30 @@
+typedef struct Xelem Xelem;
+typedef struct Xattr Xattr;
+
+struct Xelem
+{
+	char  *n;
+	char  *v;
+	Xattr *a;
+	Xelem *ch;
+	Xelem *next;
+	void  *priv;
+};
+
+struct Xattr
+{
+	char  *n;
+	char  *v;
+	Xattr *next;
+};
+
+enum
+{
+	Xmlstartonly = 1,
+};
+
+Xelem *xmlread(Biobuf *b, int flags);
+void xmlfree(Xelem *x);
+Xelem *xmlget(Xelem *x, char *path, ...);
+Xattr *xmlgetattr(Xattr *a, char *n);
+void xmlprint(Xelem *x, int fd);
--