aboutsummaryrefslogtreecommitdiff
path: root/src/cmd/smugfs
diff options
context:
space:
mode:
authorRuss Cox <rsc@swtch.com>2008-08-03 07:42:27 -0700
committerRuss Cox <rsc@swtch.com>2008-08-03 07:42:27 -0700
commit18824b586835525594cde126fbc90b8281d5af8b (patch)
treee8b69c05eda4543b6d2f3d30777abe6109b48b7f /src/cmd/smugfs
parent3d36f4437348227c5bad62587dc12b5fd4a3e95e (diff)
downloadplan9port-18824b586835525594cde126fbc90b8281d5af8b.tar.gz
plan9port-18824b586835525594cde126fbc90b8281d5af8b.tar.bz2
plan9port-18824b586835525594cde126fbc90b8281d5af8b.zip
smugfs(4): new program
Diffstat (limited to 'src/cmd/smugfs')
-rw-r--r--src/cmd/smugfs/COPYRIGHT17
-rw-r--r--src/cmd/smugfs/NOTES22
-rw-r--r--src/cmd/smugfs/a.h190
-rw-r--r--src/cmd/smugfs/cache.c149
-rw-r--r--src/cmd/smugfs/download.c105
-rw-r--r--src/cmd/smugfs/fs.c1853
-rw-r--r--src/cmd/smugfs/http.c237
-rw-r--r--src/cmd/smugfs/icache.c171
-rw-r--r--src/cmd/smugfs/json.c555
-rw-r--r--src/cmd/smugfs/jsonrpc.c244
-rw-r--r--src/cmd/smugfs/log.c120
-rw-r--r--src/cmd/smugfs/main.c108
-rw-r--r--src/cmd/smugfs/mkfile21
-rw-r--r--src/cmd/smugfs/openssl.c98
-rw-r--r--src/cmd/smugfs/tcp.c50
-rw-r--r--src/cmd/smugfs/util.c81
16 files changed, 4021 insertions, 0 deletions
diff --git a/src/cmd/smugfs/COPYRIGHT b/src/cmd/smugfs/COPYRIGHT
new file mode 100644
index 00000000..0741ccf5
--- /dev/null
+++ b/src/cmd/smugfs/COPYRIGHT
@@ -0,0 +1,17 @@
+The files in this directory are subject to the following license.
+
+The author of this software is Russ Cox.
+
+ Copyright (c) 2008 Russ Cox
+
+Permission to use, copy, modify, and distribute this software for any
+purpose without fee is hereby granted, provided that this entire notice
+is included in all copies of any software which is or includes a copy
+or modification of this software and in all copies of the supporting
+documentation for such software.
+
+THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
+WARRANTY. IN PARTICULAR, THE AUTHOR MAKES NO REPRESENTATION OR WARRANTY
+OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS SOFTWARE OR ITS
+FITNESS FOR ANY PARTICULAR PURPOSE.
+
diff --git a/src/cmd/smugfs/NOTES b/src/cmd/smugfs/NOTES
new file mode 100644
index 00000000..b8899915
--- /dev/null
+++ b/src/cmd/smugfs/NOTES
@@ -0,0 +1,22 @@
+
+* Threading:
+
+Uploads run in parallel with main fs operation.
+Otherwise, main fs operation is single-threaded.
+Could multi-thread the rest but would have to lock the
+cache properly first.
+
+Right now, only one upload at a time.
+Could have more by kicking off multiple
+uploader procs.
+
+* Implement subcategories.
+
+* Implement renames of categories.
+
+* Implement renames of albums.
+
+* Implement album settings file.
+
+* Implement image settings file.
+
diff --git a/src/cmd/smugfs/a.h b/src/cmd/smugfs/a.h
new file mode 100644
index 00000000..fa65002d
--- /dev/null
+++ b/src/cmd/smugfs/a.h
@@ -0,0 +1,190 @@
+#include <u.h>
+#include <errno.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <auth.h>
+#include <9p.h>
+#include <libsec.h>
+
+#define APIKEY "G9ANE2zvCozKEoLQ5qaR1AUtcE5YpuDj"
+#define HOST "api.smugmug.com"
+#define UPLOAD_HOST "upload.smugmug.com"
+#define API_VERSION "1.2.1"
+#define PATH "/services/api/json/" API_VERSION "/"
+#define USER_AGENT "smugfs (part of Plan 9 from User Space)"
+
+void* emalloc(int);
+void* erealloc(void*, int);
+char* estrdup(char*);
+int urlencodefmt(Fmt*);
+int timefmt(Fmt*);
+int writen(int, void*, int);
+
+
+// Generic cache
+
+typedef struct Cache Cache;
+typedef struct CEntry CEntry;
+
+struct CEntry
+{
+ char *name;
+ struct {
+ CEntry *next;
+ CEntry *prev;
+ } list;
+ struct {
+ CEntry *next;
+ } hash;
+};
+
+Cache *newcache(int sizeofentry, int maxentry, void (*cefree)(CEntry*));
+CEntry *cachelookup(Cache*, char*, int);
+void cacheflush(Cache*, char*);
+
+// JSON parser
+
+typedef struct Json Json;
+
+enum
+{
+ Jstring,
+ Jnumber,
+ Jobject,
+ Jarray,
+ Jtrue,
+ Jfalse,
+ Jnull
+};
+
+struct Json
+{
+ int ref;
+ int type;
+ char *string;
+ double number;
+ char **name;
+ Json **value;
+ int len;
+};
+
+void jclose(Json*);
+Json* jincref(Json*);
+vlong jint(Json*);
+Json* jlookup(Json*, char*);
+double jnumber(Json*);
+int jsonfmt(Fmt*);
+int jstrcmp(Json*, char*);
+char* jstring(Json*);
+Json* jwalk(Json*, char*);
+Json* parsejson(char*);
+
+
+// Wrapper to hide whether we're using OpenSSL for HTTPS.
+
+typedef struct Protocol Protocol;
+typedef struct Pfd Pfd;
+struct Protocol
+{
+ Pfd *(*connect)(char *host);
+ int (*read)(Pfd*, void*, int);
+ int (*write)(Pfd*, void*, int);
+ void (*close)(Pfd*);
+};
+
+Protocol http;
+Protocol https;
+
+
+// HTTP library
+
+typedef struct HTTPHeader HTTPHeader;
+struct HTTPHeader
+{
+ int code;
+ char proto[100];
+ char codedesc[100];
+ vlong contentlength;
+ char contenttype[100];
+};
+
+char *httpreq(Protocol *proto, char *host, char *request, HTTPHeader *hdr, int rfd, vlong rlength);
+int httptofile(Protocol *proto, char *host, char *req, HTTPHeader *hdr, int wfd);
+
+
+// URL downloader - caches in files on disk
+
+int download(char *url, HTTPHeader *hdr);
+void downloadflush(char*);
+
+// JSON RPC
+
+enum
+{
+ MaxResponse = 1<<29,
+};
+
+Json* jsonrpc(Protocol *proto, char *host, char *path, char *method, char *name1, va_list arg, int usecache);
+Json* jsonupload(Protocol *proto, char *host, char *req, int rfd, vlong rlength);
+void jcacheflush(char*);
+
+extern int chattyhttp;
+
+
+// SmugMug RPC
+
+#ifdef __GNUC__
+#define check_nil __attribute__((sentinel))
+#else
+#define check_nil
+#endif
+
+Json* smug(char *method, char *name1, ...) check_nil; // cached, http
+Json* ncsmug(char *method, char *name1, ...) check_nil; // not cached, https
+
+
+// Session information
+
+extern Json *userinfo;
+extern char *sessid;
+
+
+// File system
+
+extern Srv xsrv;
+void xinit(void);
+extern int nickindex(char*);
+
+// Logging
+
+typedef struct Logbuf Logbuf;
+struct Logbuf
+{
+ Req *wait;
+ Req **waitlast;
+ int rp;
+ int wp;
+ char *msg[128];
+};
+
+extern void lbkick(Logbuf*);
+extern void lbappend(Logbuf*, char*, ...);
+extern void lbvappend(Logbuf*, char*, va_list);
+/* #pragma varargck argpos lbappend 2 */
+extern void lbread(Logbuf*, Req*);
+extern void lbflush(Logbuf*, Req*);
+/* #pragma varargck argpos flog 1 */
+
+extern void rpclog(char*, ...);
+extern void rpclogflush(Req*);
+extern void rpclogread(Req*);
+extern void rpclogwrite(Req*);
+
+enum
+{
+ STACKSIZE = 32768
+};
+
+extern int printerrors;
+
diff --git a/src/cmd/smugfs/cache.c b/src/cmd/smugfs/cache.c
new file mode 100644
index 00000000..2adf8b9b
--- /dev/null
+++ b/src/cmd/smugfs/cache.c
@@ -0,0 +1,149 @@
+#include "a.h"
+
+struct Cache
+{
+ CEntry **hash;
+ int nhash;
+ CEntry *head;
+ CEntry *tail;
+ int nentry;
+ int maxentry;
+ int sizeofentry;
+ void (*cefree)(CEntry*);
+};
+
+static void
+nop(CEntry *ce)
+{
+}
+
+static uint
+hash(const char *s)
+{
+ uint h;
+ uchar *p;
+
+ h = 0;
+ for(p=(uchar*)s; *p; p++)
+ h = h*37 + *p;
+ return h;
+}
+
+Cache*
+newcache(int sizeofentry, int maxentry, void (*cefree)(CEntry*))
+{
+ Cache *c;
+ int i;
+
+ assert(sizeofentry >= sizeof(CEntry));
+ c = emalloc(sizeof *c);
+ c->sizeofentry = sizeofentry;
+ c->maxentry = maxentry;
+ c->nentry = 0;
+ for(i=1; i<maxentry; i<<=1)
+ ;
+ c->nhash = i;
+ c->hash = emalloc(c->nhash * sizeof c->hash[0]);
+ if(cefree == nil)
+ cefree = nop;
+ c->cefree = cefree;
+ return c;
+}
+
+static void
+popout(Cache *c, CEntry *e)
+{
+ if(e->list.prev)
+ e->list.prev->list.next = e->list.next;
+ else
+ c->head = e->list.next;
+ if(e->list.next)
+ e->list.next->list.prev = e->list.prev;
+ else
+ c->tail = e->list.prev;
+}
+
+static void
+insertfront(Cache *c, CEntry *e)
+{
+ e->list.next = c->head;
+ c->head = e;
+ if(e->list.next)
+ e->list.next->list.prev = e;
+ else
+ c->tail = e;
+}
+
+static void
+movetofront(Cache *c, CEntry *e)
+{
+ popout(c, e);
+ insertfront(c, e);
+}
+
+static CEntry*
+evict(Cache *c)
+{
+ CEntry *e;
+
+ e = c->tail;
+ popout(c, e);
+ c->cefree(e);
+ free(e->name);
+ e->name = nil;
+ memset(e, 0, c->sizeofentry);
+ insertfront(c, e);
+ return e;
+}
+
+CEntry*
+cachelookup(Cache *c, char *name, int create)
+{
+ int h;
+ CEntry *e;
+
+ h = hash(name) % c->nhash;
+ for(e=c->hash[h]; e; e=e->hash.next){
+ if(strcmp(name, e->name) == 0){
+ movetofront(c, e);
+ return e;
+ }
+ }
+
+ if(!create)
+ return nil;
+
+ if(c->nentry >= c->maxentry)
+ e = evict(c);
+ else{
+ e = emalloc(c->sizeofentry);
+ insertfront(c, e);
+ c->nentry++;
+ }
+ e->name = estrdup(name);
+ h = hash(name) % c->nhash;
+ e->hash.next = c->hash[h];
+ c->hash[h] = e;
+ return e;
+}
+
+void
+cacheflush(Cache *c, char *substr)
+{
+ CEntry **l, *e;
+ int i;
+
+ for(i=0; i<c->nhash; i++){
+ for(l=&c->hash[i]; (e=*l); ){
+ if(substr == nil || strstr(e->name, substr)){
+ *l = e->hash.next;
+ c->nentry--;
+ popout(c, e);
+ c->cefree(e);
+ free(e->name);
+ free(e);
+ }else
+ l = &e->hash.next;
+ }
+ }
+}
diff --git a/src/cmd/smugfs/download.c b/src/cmd/smugfs/download.c
new file mode 100644
index 00000000..e23d49e1
--- /dev/null
+++ b/src/cmd/smugfs/download.c
@@ -0,0 +1,105 @@
+#include "a.h"
+
+typedef struct DEntry DEntry;
+struct DEntry
+{
+ CEntry ce;
+ HTTPHeader hdr;
+ char *tmpfile;
+ int fd;
+};
+
+static void
+dfree(CEntry *ce)
+{
+ DEntry *d;
+
+ d = (DEntry*)ce;
+ if(d->tmpfile){
+ remove(d->tmpfile);
+ free(d->tmpfile);
+ close(d->fd);
+ }
+}
+
+static Cache *downloadcache;
+
+static char*
+parseurl(char *url, char **path)
+{
+ char *host, *p;
+ int len;
+
+ if(memcmp(url, "http://", 7) != 0)
+ return nil;
+ p = strchr(url+7, '/');
+ if(p == nil)
+ p = url+strlen(url);
+ len = p - (url+7);
+ host = emalloc(len+1);
+ memmove(host, url+7, len);
+ host[len] = 0;
+ if(*p == 0)
+ p = "/";
+ *path = p;
+ return host;
+}
+
+int
+download(char *url, HTTPHeader *hdr)
+{
+ DEntry *d;
+ char *host, *path;
+ char buf[] = "/var/tmp/smugfs.XXXXXX";
+ char *req;
+ int fd;
+ Fmt fmt;
+
+ if(downloadcache == nil)
+ downloadcache = newcache(sizeof(DEntry), 128, dfree);
+
+ host = parseurl(url, &path);
+ if(host == nil)
+ return -1;
+
+ d = (DEntry*)cachelookup(downloadcache, url, 1);
+ if(d->tmpfile){
+ free(host);
+ *hdr = d->hdr;
+ return dup(d->fd, -1);
+ }
+ d->fd = -1; // paranoia
+
+ if((fd = opentemp(buf, ORDWR|ORCLOSE)) < 0){
+ free(host);
+ return -1;
+ }
+
+ fmtstrinit(&fmt);
+ fmtprint(&fmt, "GET %s HTTP/1.0\r\n", path);
+ fmtprint(&fmt, "Host: %s\r\n", host);
+ fmtprint(&fmt, "User-Agent: " USER_AGENT "\r\n");
+ fmtprint(&fmt, "\r\n");
+ req = fmtstrflush(&fmt);
+
+ fprint(2, "Get %s\n", url);
+
+ if(httptofile(&http, host, req, hdr, fd) < 0){
+ free(host);
+ free(req);
+ return -1;
+ }
+ free(host);
+ free(req);
+ d->tmpfile = estrdup(buf);
+ d->fd = dup(fd, -1);
+ d->hdr = *hdr;
+ return fd;
+}
+
+void
+downloadflush(char *substr)
+{
+ if(downloadcache)
+ cacheflush(downloadcache, substr);
+}
diff --git a/src/cmd/smugfs/fs.c b/src/cmd/smugfs/fs.c
new file mode 100644
index 00000000..52b82051
--- /dev/null
+++ b/src/cmd/smugfs/fs.c
@@ -0,0 +1,1853 @@
+#include "a.h"
+
+enum
+{
+ Qroot = 0, // /smug/
+ Qctl, // /smug/ctl
+ Qrpclog, // /smug/rpclog
+ Quploads, // /smug/uploads
+ Qnick, // /smug/nick/
+ Qnickctl, // /smug/nick/ctl
+ Qalbums, // /smug/nick/albums/
+ Qalbumsctl, // /smug/nick/albums/ctl
+ Qcategory, // /smug/nick/Category/
+ Qcategoryctl, // /smug/nick/Category/ctl
+ Qalbum, // /smug/nick/Category/Album/
+ Qalbumctl, // /smug/nick/Category/Album/ctl
+ Qalbumsettings, // /smug/nick/Category/Album/settings
+ Quploadfile, // /smug/nick/Category/Album/upload/file.jpg
+ Qimage, // /smug/nick/Category/Album/Image/
+ Qimagectl, // /smug/nick/Category/Album/Image/ctl
+ Qimageexif, // /smug/nick/Category/Album/Image/exif
+ Qimagesettings, // /smug/nick/Category/Album/Image/settings
+ Qimageurl, // /smug/nick/Category/Album/Image/url
+ Qimagefile, // /smug/nick/Category/Album/Image/file.jpg
+};
+
+void
+mylock(Lock *lk)
+{
+ lock(lk);
+ fprint(2, "locked from %p\n", getcallerpc(&lk));
+}
+
+void
+myunlock(Lock *lk)
+{
+ unlock(lk);
+ fprint(2, "unlocked from %p\n", getcallerpc(&lk));
+}
+
+//#define lock mylock
+//#define unlock myunlock
+
+typedef struct Upload Upload;
+
+typedef struct SmugFid SmugFid;
+struct SmugFid
+{
+ int type;
+ int nickid;
+ vlong category; // -1 for "albums"
+ vlong album;
+ char *albumkey;
+ vlong image;
+ char *imagekey;
+ Upload *upload;
+ int upwriter;
+};
+
+#define QTYPE(p) ((p)&0xFF)
+#define QARG(p) ((p)>>8)
+#define QPATH(p, q) ((p)|((q)<<8))
+
+char **nick;
+int nnick;
+
+struct Upload
+{
+ Lock lk;
+ int fd;
+ char *name;
+ char *file;
+ vlong album;
+ vlong length;
+ char *albumkey;
+ int size;
+ int ready;
+ int nwriters;
+ int uploaded;
+ int ref;
+ int uploading;
+};
+
+Upload **up;
+int nup;
+QLock uploadlock;
+Rendez uploadrendez;
+
+void uploader(void*);
+
+Upload*
+newupload(SmugFid *sf, char *name)
+{
+ Upload *u;
+ int fd, i;
+ char tmp[] = "/var/tmp/smugfs.XXXXXX";
+
+ if((fd = opentemp(tmp, ORDWR)) < 0)
+ return nil;
+ qlock(&uploadlock);
+ for(i=0; i<nup; i++){
+ u = up[i];
+ lock(&u->lk);
+ if(u->ref == 0){
+ u->ref = 1;
+ goto Reuse;
+ }
+ unlock(&u->lk);
+ }
+ if(nup == 0){
+ uploadrendez.l = &uploadlock;
+ proccreate(uploader, nil, STACKSIZE);
+ }
+ u = emalloc(sizeof *u);
+ lock(&u->lk);
+ u->ref = 1;
+ up = erealloc(up, (nup+1)*sizeof up[0]);
+ up[nup++] = u;
+Reuse:
+ qunlock(&uploadlock);
+ u->fd = fd;
+ u->name = estrdup(name);
+ u->file = estrdup(tmp);
+ u->album = sf->album;
+ u->albumkey = estrdup(sf->albumkey);
+ u->nwriters = 1;
+ unlock(&u->lk);
+ return u;
+}
+
+void
+closeupload(Upload *u)
+{
+ lock(&u->lk);
+fprint(2, "close %p from %p: %d\n", u, getcallerpc(&u), u->ref);
+ if(--u->ref > 0){
+ unlock(&u->lk);
+ return;
+ }
+ if(u->ref < 0)
+ abort();
+ if(u->fd >= 0){
+ close(u->fd);
+ u->fd = -1;
+ }
+ if(u->name){
+ free(u->name);
+ u->name = nil;
+ }
+ if(u->file){
+ remove(u->file);
+ free(u->file);
+ u->file = nil;
+ }
+ u->album = 0;
+ if(u->albumkey){
+ free(u->albumkey);
+ u->albumkey = nil;
+ }
+ u->size = 0;
+ u->ready = 0;
+ u->nwriters = 0;
+ u->uploaded = 0;
+ u->uploading = 0;
+ u->length = 0;
+ unlock(&u->lk);
+}
+
+Upload*
+getuploadindex(SmugFid *sf, int *index)
+{
+ int i;
+ Upload *u;
+
+ qlock(&uploadlock);
+ for(i=0; i<nup; i++){
+ u = up[i];
+ lock(&u->lk);
+ if(u->ref > 0 && !u->uploaded && u->album == sf->album && (*index)-- == 0){
+ qunlock(&uploadlock);
+ u->ref++;
+fprint(2, "bump %p from %p: %d\n", u, getcallerpc(&sf), u->ref);
+ unlock(&u->lk);
+ return u;
+ }
+ unlock(&u->lk);
+ }
+ qunlock(&uploadlock);
+ return nil;
+}
+
+Upload*
+getuploadname(SmugFid *sf, char *name)
+{
+ int i;
+ Upload *u;
+
+ qlock(&uploadlock);
+ for(i=0; i<nup; i++){
+ u = up[i];
+ lock(&u->lk);
+ if(u->ref > 0 && !u->uploaded && u->album == sf->album && strcmp(name, u->name) == 0){
+ qunlock(&uploadlock);
+ u->ref++;
+fprint(2, "bump %p from %p: %d\n", u, getcallerpc(&sf), u->ref);
+ unlock(&u->lk);
+ return u;
+ }
+ unlock(&u->lk);
+ }
+ qunlock(&uploadlock);
+ return nil;
+}
+
+void doupload(Upload*);
+
+void
+uploader(void *v)
+{
+ int i, did;
+ Upload *u;
+
+ qlock(&uploadlock);
+ for(;;){
+ did = 0;
+ for(i=0; i<nup; i++){
+ u = up[i];
+ lock(&u->lk);
+ if(u->ref > 0 && u->ready && !u->uploading && !u->uploaded){
+ u->uploading = 1;
+ unlock(&u->lk);
+ qunlock(&uploadlock);
+ doupload(u);
+ closeupload(u);
+fprint(2, "done %d\n", u->ref);
+ did = 1;
+ qlock(&uploadlock);
+ }else
+ unlock(&u->lk);
+ }
+ if(!did)
+ rsleep(&uploadrendez);
+ }
+}
+
+void
+kickupload(Upload *u)
+{
+ Dir *d;
+
+ lock(&u->lk);
+ if((d = dirfstat(u->fd)) != nil)
+ u->length = d->length;
+ close(u->fd);
+ u->fd = -1;
+ u->ref++;
+fprint(2, "kick %p from %p: %d\n", u, getcallerpc(&u), u->ref);
+ u->ready = 1;
+ unlock(&u->lk);
+ qlock(&uploadlock);
+ rwakeup(&uploadrendez);
+ qunlock(&uploadlock);
+}
+
+void
+doupload(Upload *u)
+{
+ Dir *d;
+ vlong datalen;
+ Fmt fmt;
+ char *req;
+ char buf[8192];
+ int n, total;
+ uchar digest[MD5dlen];
+ DigestState ds;
+ Json *jv;
+
+ if((u->fd = open(u->file, OREAD)) < 0){
+ fprint(2, "cannot reopen temporary file %s: %r\n", u->file);
+ return;
+ }
+ if((d = dirfstat(u->fd)) == nil){
+ fprint(2, "fstat: %r\n");
+ return;
+ }
+ datalen = d->length;
+ free(d);
+
+ memset(&ds, 0, sizeof ds);
+ seek(u->fd, 0, 0);
+ total = 0;
+ while((n = read(u->fd, buf, sizeof buf)) > 0){
+ md5((uchar*)buf, n, nil, &ds);
+ total += n;
+ }
+ if(total != datalen){
+ fprint(2, "bad total: %lld %lld\n", total, datalen);
+ return;
+ }
+ md5(nil, 0, digest, &ds);
+
+ fmtstrinit(&fmt);
+ fmtprint(&fmt, "PUT /%s HTTP/1.0\r\n", u->name);
+ fmtprint(&fmt, "Content-Length: %lld\r\n", datalen);
+ fmtprint(&fmt, "Content-MD5: %.16lH\r\n", digest);
+ fmtprint(&fmt, "X-Smug-SessionID: %s\r\n", sessid);
+ fmtprint(&fmt, "X-Smug-Version: %s\r\n", API_VERSION);
+ fmtprint(&fmt, "X-Smug-ResponseType: JSON\r\n");
+ // Can send X-Smug-ImageID instead to replace existing files.
+ fmtprint(&fmt, "X-Smug-AlbumID: %lld\r\n", u->album);
+ fmtprint(&fmt, "X-Smug-FileName: %s\r\n", u->name);
+ fmtprint(&fmt, "\r\n");
+ req = fmtstrflush(&fmt);
+
+ seek(u->fd, 0, 0);
+ jv = jsonupload(&http, UPLOAD_HOST, req, u->fd, datalen);
+ free(req);
+ if(jv == nil){
+ fprint(2, "upload: %r\n");
+ return;
+ }
+
+ close(u->fd);
+ remove(u->file);
+ free(u->file);
+ u->file = nil;
+ u->fd = -1;
+ u->uploaded = 1;
+ rpclog("uploaded: %J", jv);
+ jclose(jv);
+}
+
+int
+nickindex(char *name)
+{
+ int i;
+ Json *v;
+
+ for(i=0; i<nnick; i++)
+ if(strcmp(nick[i], name) == 0)
+ return i;
+ v = smug("smugmug.users.getTree", "NickName", name, nil);
+ if(v == nil)
+ return -1;
+ nick = erealloc(nick, (nnick+1)*sizeof nick[0]);
+ nick[nnick] = estrdup(name);
+ return nnick++;
+}
+
+char*
+nickname(int i)
+{
+ if(i < 0 || i >= nnick)
+ return nil;
+ return nick[i];
+}
+
+void
+responderrstr(Req *r)
+{
+ char err[ERRMAX];
+
+ rerrstr(err, sizeof err);
+ respond(r, err);
+}
+
+static char*
+xclone(Fid *oldfid, Fid *newfid)
+{
+ SmugFid *sf;
+
+ if(oldfid->aux == nil)
+ return nil;
+
+ sf = emalloc(sizeof *sf);
+ *sf = *(SmugFid*)oldfid->aux;
+ sf->upload = nil;
+ sf->upwriter = 0;
+ if(sf->albumkey)
+ sf->albumkey = estrdup(sf->albumkey);
+ if(sf->imagekey)
+ sf->imagekey = estrdup(sf->imagekey);
+ newfid->aux = sf;
+ return nil;
+}
+
+static void
+xdestroyfid(Fid *fid)
+{
+ SmugFid *sf;
+
+ sf = fid->aux;
+ free(sf->albumkey);
+ free(sf->imagekey);
+ if(sf->upload){
+ if(sf->upwriter && --sf->upload->nwriters == 0){
+ fprint(2, "should upload %s\n", sf->upload->name);
+ kickupload(sf->upload);
+ }
+ closeupload(sf->upload);
+ sf->upload = nil;
+ }
+ free(sf);
+}
+
+static Json*
+getcategories(SmugFid *sf)
+{
+ Json *v, *w;
+
+ v = smug("smugmug.categories.get", "NickName", nickname(sf->nickid), nil);
+ w = jincref(jwalk(v, "Categories"));
+ jclose(v);
+ return w;
+}
+
+static Json*
+getcategorytree(SmugFid *sf)
+{
+ Json *v, *w;
+
+ v = smug("smugmug.users.getTree", "NickName", nickname(sf->nickid), nil);
+ w = jincref(jwalk(v, "Categories"));
+ jclose(v);
+ return w;
+}
+
+static Json*
+getcategory(SmugFid *sf, vlong id)
+{
+ int i;
+ Json *v, *w;
+
+ v = getcategorytree(sf);
+ if(v == nil)
+ return nil;
+ for(i=0; i<v->len; i++){
+ if(jint(jwalk(v->value[i], "id")) == id){
+ w = jincref(v->value[i]);
+ jclose(v);
+ return w;
+ }
+ }
+ jclose(v);
+ return nil;
+}
+
+static vlong
+getcategoryid(SmugFid *sf, char *name)
+{
+ int i;
+ vlong id;
+ Json *v;
+
+ v = getcategories(sf);
+ if(v == nil)
+ return -1;
+ for(i=0; i<v->len; i++){
+ if(jstrcmp(jwalk(v->value[i], "Name"), name) == 0){
+ id = jint(jwalk(v->value[i], "id"));
+ if(id < 0){
+ jclose(v);
+ return -1;
+ }
+ jclose(v);
+ return id;
+ }
+ }
+ jclose(v);
+ return -1;
+}
+
+static vlong
+getcategoryindex(SmugFid *sf, int i)
+{
+ Json *v;
+ vlong id;
+
+ v = getcategories(sf);
+ if(v == nil)
+ return -1;
+ if(i < 0 || i >= v->len){
+ jclose(v);
+ return -1;
+ }
+ id = jint(jwalk(v->value[i], "id"));
+ jclose(v);
+ return id;
+}
+
+static Json*
+getalbum(SmugFid *sf, vlong albumid, char *albumkey)
+{
+ char id[50];
+ Json *v, *w;
+
+ snprint(id, sizeof id, "%lld", albumid);
+ v = smug("smugmug.albums.getInfo",
+ "AlbumID", id, "AlbumKey", albumkey,
+ "NickName", nickname(sf->nickid), nil);
+ w = jincref(jwalk(v, "Album"));
+ jclose(v);
+ return w;
+}
+
+static Json*
+getalbums(SmugFid *sf)
+{
+ Json *v, *w;
+
+ if(sf->category >= 0)
+ v = getcategory(sf, sf->category);
+ else
+ v = smug("smugmug.albums.get",
+ "NickName", nickname(sf->nickid), nil);
+ w = jincref(jwalk(v, "Albums"));
+ jclose(v);
+ return w;
+}
+
+static vlong
+getalbumid(SmugFid *sf, char *name, char **keyp)
+{
+ int i;
+ vlong id;
+ Json *v;
+ char *key;
+
+ v = getalbums(sf);
+ if(v == nil)
+ return -1;
+ for(i=0; i<v->len; i++){
+ if(jstrcmp(jwalk(v->value[i], "Title"), name) == 0){
+ id = jint(jwalk(v->value[i], "id"));
+ key = jstring(jwalk(v->value[i], "Key"));
+ if(id < 0 || key == nil){
+ jclose(v);
+ return -1;
+ }
+ if(keyp)
+ *keyp = estrdup(key);
+ jclose(v);
+ return id;
+ }
+ }
+ jclose(v);
+ return -1;
+}
+
+static vlong
+getalbumindex(SmugFid *sf, int i, char **keyp)
+{
+ vlong id;
+ Json *v;
+ char *key;
+
+ v = getalbums(sf);
+ if(v == nil)
+ return -1;
+ if(i < 0 || i >= v->len){
+ jclose(v);
+ return -1;
+ }
+ id = jint(jwalk(v->value[i], "id"));
+ key = jstring(jwalk(v->value[i], "Key"));
+ if(id < 0 || key == nil){
+ jclose(v);
+ return -1;
+ }
+ if(keyp)
+ *keyp = estrdup(key);
+ jclose(v);
+ return id;
+}
+
+static Json*
+getimages(SmugFid *sf, vlong albumid, char *albumkey)
+{
+ char id[50];
+ Json *v, *w;
+
+ snprint(id, sizeof id, "%lld", albumid);
+ v = smug("smugmug.images.get",
+ "AlbumID", id, "AlbumKey", albumkey,
+ "NickName", nickname(sf->nickid), nil);
+ w = jincref(jwalk(v, "Images"));
+ jclose(v);
+ return w;
+}
+
+static vlong
+getimageid(SmugFid *sf, char *name, char **keyp)
+{
+ int i;
+ vlong id;
+ Json *v;
+ char *p;
+ char *key;
+
+ id = strtol(name, &p, 10);
+ if(*p != 0 || *name == 0)
+ return -1;
+
+ v = getimages(sf, sf->album, sf->albumkey);
+ if(v == nil)
+ return -1;
+ for(i=0; i<v->len; i++){
+ if(jint(jwalk(v->value[i], "id")) == id){
+ key = jstring(jwalk(v->value[i], "Key"));
+ if(key == nil){
+ jclose(v);
+ return -1;
+ }
+ if(keyp)
+ *keyp = estrdup(key);
+ jclose(v);
+ return id;
+ }
+ }
+ jclose(v);
+ return -1;
+}
+
+static Json*
+getimageinfo(SmugFid *sf, vlong imageid, char *imagekey)
+{
+ char id[50];
+ Json *v, *w;
+
+ snprint(id, sizeof id, "%lld", imageid);
+ v = smug("smugmug.images.getInfo",
+ "ImageID", id, "ImageKey", imagekey,
+ "NickName", nickname(sf->nickid), nil);
+ w = jincref(jwalk(v, "Image"));
+ jclose(v);
+ return w;
+}
+
+static Json*
+getimageexif(SmugFid *sf, vlong imageid, char *imagekey)
+{
+ char id[50];
+ Json *v, *w;
+
+ snprint(id, sizeof id, "%lld", imageid);
+ v = smug("smugmug.images.getEXIF",
+ "ImageID", id, "ImageKey", imagekey,
+ "NickName", nickname(sf->nickid), nil);
+ w = jincref(jwalk(v, "Image"));
+ jclose(v);
+ return w;
+}
+
+static vlong
+getimageindex(SmugFid *sf, int i, char **keyp)
+{
+ vlong id;
+ Json *v;
+ char *key;
+
+ v = getimages(sf, sf->album, sf->albumkey);
+ if(v == nil)
+ return -1;
+ if(i < 0 || i >= v->len){
+ jclose(v);
+ return -1;
+ }
+ id = jint(jwalk(v->value[i], "id"));
+ key = jstring(jwalk(v->value[i], "Key"));
+ if(id < 0 || key == nil){
+ jclose(v);
+ return -1;
+ }
+ if(keyp)
+ *keyp = estrdup(key);
+ jclose(v);
+ return id;
+}
+
+static char*
+categoryname(SmugFid *sf)
+{
+ Json *v;
+ char *s;
+
+ v = getcategory(sf, sf->category);
+ s = jstring(jwalk(v, "Name"));
+ if(s)
+ s = estrdup(s);
+ jclose(v);
+ return s;
+}
+
+static char*
+albumname(SmugFid *sf)
+{
+ Json *v;
+ char *s;
+
+ v = getalbum(sf, sf->album, sf->albumkey);
+ s = jstring(jwalk(v, "Title"));
+ if(s)
+ s = estrdup(s);
+ jclose(v);
+ return s;
+}
+
+static char*
+imagename(SmugFid *sf)
+{
+ char *s;
+ Json *v;
+
+ v = getimageinfo(sf, sf->image, sf->imagekey);
+ s = jstring(jwalk(v, "FileName"));
+ if(s && s[0])
+ s = estrdup(s);
+ else
+ s = smprint("%lld.jpg", sf->image); // TODO: use Format
+ jclose(v);
+ return s;
+}
+
+static vlong
+imagelength(SmugFid *sf)
+{
+ vlong length;
+ Json *v;
+
+ v = getimageinfo(sf, sf->image, sf->imagekey);
+ length = jint(jwalk(v, "Size"));
+ jclose(v);
+ return length;
+}
+
+static struct {
+ char *key;
+ char *name;
+} urls[] = {
+ "AlbumURL", "album",
+ "TinyURL", "tiny",
+ "ThumbURL", "thumb",
+ "SmallURL", "small",
+ "MediumURL", "medium",
+ "LargeURL", "large",
+ "XLargeURL", "xlarge",
+ "X2LargeURL", "xxlarge",
+ "X3LargeURL", "xxxlarge",
+ "OriginalURL", "original",
+};
+
+static char*
+imageurl(SmugFid *sf)
+{
+ Json *v;
+ char *s;
+ int i;
+
+ v = getimageinfo(sf, sf->image, sf->imagekey);
+ for(i=nelem(urls)-1; i>=0; i--){
+ if((s = jstring(jwalk(v, urls[i].key))) != nil){
+ s = estrdup(s);
+ jclose(v);
+ return s;
+ }
+ }
+ jclose(v);
+ return nil;
+}
+
+static char* imagestrings[] =
+{
+ "Caption",
+ "LastUpdated",
+ "FileName",
+ "MD5Sum",
+ "Watermark",
+ "Format",
+ "Keywords",
+ "Date",
+ "AlbumURL",
+ "TinyURL",
+ "ThumbURL",
+ "SmallURL",
+ "MediumURL",
+ "LargeURL",
+ "XLargeURL",
+ "X2LargeURL",
+ "X3LargeURL",
+ "OriginalURL",
+ "Album",
+};
+
+static char* albumbools[] =
+{
+ "Public",
+ "Printable",
+ "Filenames",
+ "Comments",
+ "External",
+ "Originals",
+ "EXIF",
+ "Share",
+ "SortDirection",
+ "FamilyEdit",
+ "FriendEdit",
+ "HideOwner",
+ "CanRank",
+ "Clean",
+ "Geography",
+ "SmugSearchable",
+ "WorldSearchable",
+ "SquareThumbs",
+ "X2Larges",
+ "X3Larges",
+};
+
+static char* albumstrings[] =
+{
+ "Description"
+ "Keywords",
+ "Password",
+ "PasswordHint",
+ "SortMethod",
+ "LastUpdated",
+};
+
+static char*
+readctl(SmugFid *sf)
+{
+ int i;
+ Upload *u;
+ char *s;
+ Json *v, *vv;
+ Fmt fmt;
+
+ v = nil;
+ switch(sf->type){
+ case Qctl:
+ return smprint("%#J\n", userinfo);
+
+ case Quploads:
+ fmtstrinit(&fmt);
+ qlock(&uploadlock);
+ for(i=0; i<nup; i++){
+ u = up[i];
+ lock(&u->lk);
+ if(u->ready && !u->uploaded && u->ref > 0)
+ fmtprint(&fmt, "%s %s%s\n", u->name, u->file, u->uploading ? " [uploading]" : "");
+ unlock(&u->lk);
+ }
+ qunlock(&uploadlock);
+ return fmtstrflush(&fmt);
+
+ case Qnickctl:
+ v = getcategories(sf);
+ break;
+
+ case Qcategoryctl:
+ v = getcategory(sf, sf->category);
+ break;
+
+ case Qalbumctl:
+ v = getimages(sf, sf->album, sf->albumkey);
+ break;
+
+ case Qalbumsctl:
+ v = getalbums(sf);
+ break;
+
+ case Qimagectl:
+ v = getimageinfo(sf, sf->image, sf->imagekey);
+ break;
+
+ case Qimageurl:
+ v = getimageinfo(sf, sf->image, sf->imagekey);
+ fmtstrinit(&fmt);
+ for(i=0; i<nelem(urls); i++)
+ if((s = jstring(jwalk(v, urls[i].key))) != nil)
+ fmtprint(&fmt, "%s %s\n", urls[i].name, s);
+ jclose(v);
+ return fmtstrflush(&fmt);
+
+ case Qimageexif:
+ v = getimageexif(sf, sf->image, sf->imagekey);
+ break;
+
+ case Qalbumsettings:
+ v = getalbum(sf, sf->album, sf->albumkey);
+ fmtstrinit(&fmt);
+ fmtprint(&fmt, "id\t%lld\n", jint(jwalk(v, "id")));
+ // TODO: Category/id
+ // TODO: SubCategory/id
+ // TODO: Community/id
+ // TODO: Template/id
+ fmtprint(&fmt, "Highlight\t%lld\n", jint(jwalk(v, "Highlight/id")));
+ fmtprint(&fmt, "Position\t%lld\n", jint(jwalk(v, "Position")));
+ fmtprint(&fmt, "ImageCount\t%lld\n", jint(jwalk(v, "ImageCount")));
+ for(i=0; i<nelem(albumbools); i++){
+ vv = jwalk(v, albumbools[i]);
+ if(vv)
+ fmtprint(&fmt, "%s\t%J\n", albumbools[i], vv);
+ }
+ for(i=0; i<nelem(albumstrings); i++){
+ s = jstring(jwalk(v, albumstrings[i]));
+ if(s)
+ fmtprint(&fmt, "%s\t%s\n", albumstrings[i], s);
+ }
+ s = fmtstrflush(&fmt);
+ jclose(v);
+ return s;
+
+ case Qimagesettings:
+ v = getimageinfo(sf, sf->image, sf->imagekey);
+ fmtstrinit(&fmt);
+ fmtprint(&fmt, "id\t%lld\n", jint(jwalk(v, "id")));
+ fmtprint(&fmt, "Position\t%lld\n", jint(jwalk(v, "Position")));
+ fmtprint(&fmt, "Serial\t%lld\n", jint(jwalk(v, "Serial")));
+ fmtprint(&fmt, "Size\t%lld\t%lldx%lld\n",
+ jint(jwalk(v, "Size")),
+ jint(jwalk(v, "Width")),
+ jint(jwalk(v, "Height")));
+ vv = jwalk(v, "Hidden");
+ fmtprint(&fmt, "Hidden\t%J\n", vv);
+ // TODO: Album/id
+ for(i=0; i<nelem(imagestrings); i++){
+ s = jstring(jwalk(v, imagestrings[i]));
+ if(s)
+ fmtprint(&fmt, "%s\t%s\n", imagestrings[i], s);
+ }
+ s = fmtstrflush(&fmt);
+ jclose(v);
+ return s;
+ }
+
+ if(v == nil)
+ return estrdup("");
+ s = smprint("%#J\n", v);
+ jclose(v);
+ return s;
+}
+
+
+static void
+dostat(SmugFid *sf, Qid *qid, Dir *dir)
+{
+ Qid q;
+ char *name;
+ int freename;
+ ulong mode;
+ char *uid;
+ char *s;
+ vlong length;
+
+ memset(&q, 0, sizeof q);
+ name = nil;
+ freename = 0;
+ uid = "smugfs";
+ q.type = 0;
+ q.vers = 0;
+ q.path = QPATH(sf->type, sf->nickid);
+ length = 0;
+ mode = 0444;
+
+ switch(sf->type){
+ case Qroot:
+ name = "/";
+ q.type = QTDIR;
+ break;
+ case Qctl:
+ name = "ctl";
+ mode |= 0222;
+ break;
+ case Quploads:
+ name = "uploads";
+ s = readctl(sf);
+ if(s){
+ length = strlen(s);
+ free(s);
+ }
+ break;
+ case Qrpclog:
+ name = "rpclog";
+ break;
+ case Qnick:
+ name = nickname(sf->nickid);
+ q.type = QTDIR;
+ break;
+ case Qnickctl:
+ name = "ctl";
+ mode |= 0222;
+ break;
+ case Qalbums:
+ name = "albums";
+ q.type = QTDIR;
+ break;
+ case Qalbumsctl:
+ name = "ctl";
+ mode |= 0222;
+ break;
+ case Qcategory:
+ name = categoryname(sf);
+ freename = 1;
+ q.path |= QPATH(0, sf->category << 8);
+ q.type = QTDIR;
+ break;
+ case Qcategoryctl:
+ name = "ctl";
+ mode |= 0222;
+ q.path |= QPATH(0, sf->category << 8);
+ break;
+ case Qalbum:
+ name = albumname(sf);
+ freename = 1;
+ q.path |= QPATH(0, sf->album << 8);
+ q.type = QTDIR;
+ break;
+ case Qalbumctl:
+ name = "ctl";
+ mode |= 0222;
+ q.path |= QPATH(0, sf->album << 8);
+ break;
+ case Qalbumsettings:
+ name = "settings";
+ mode |= 0222;
+ q.path |= QPATH(0, sf->album << 8);
+ break;
+ case Quploadfile:
+ q.path |= QPATH(0, (uintptr)sf->upload << 8);
+ if(sf->upload){
+ Dir *dd;
+ name = sf->upload->name;
+ if(sf->upload->fd >= 0){
+ dd = dirfstat(sf->upload->fd);
+ if(dd){
+ length = dd->length;
+ free(dd);
+ }
+ }else
+ length = sf->upload->length;
+ if(!sf->upload->ready)
+ mode |= 0222;
+ }
+ break;
+ case Qimage:
+ name = smprint("%lld", sf->image);
+ freename = 1;
+ q.path |= QPATH(0, sf->image << 8);
+ q.type = QTDIR;
+ break;
+ case Qimagectl:
+ name = "ctl";
+ mode |= 0222;
+ q.path |= QPATH(0, sf->image << 8);
+ break;
+ case Qimagesettings:
+ name = "settings";
+ mode |= 0222;
+ q.path |= QPATH(0, sf->image << 8);
+ break;
+ case Qimageexif:
+ name = "exif";
+ q.path |= QPATH(0, sf->image << 8);
+ break;
+ case Qimageurl:
+ name = "url";
+ q.path |= QPATH(0, sf->image << 8);
+ break;
+ case Qimagefile:
+ name = imagename(sf);
+ freename = 1;
+ q.path |= QPATH(0, sf->image << 8);
+ length = imagelength(sf);
+ break;
+ default:
+ name = "?egreg";
+ q.path = 0;
+ break;
+ }
+
+ if(name == nil){
+ name = "???";
+ freename = 0;
+ }
+
+ if(qid)
+ *qid = q;
+ if(dir){
+ memset(dir, 0, sizeof *dir);
+ dir->name = estrdup9p(name);
+ dir->muid = estrdup9p("muid");
+ mode |= q.type<<24;
+ if(mode & DMDIR)
+ mode |= 0555;
+ dir->mode = mode;
+ dir->uid = estrdup9p(uid);
+ dir->gid = estrdup9p("smugfs");
+ dir->qid = q;
+ dir->length = length;
+ }
+ if(freename)
+ free(name);
+}
+
+static char*
+xwalk1(Fid *fid, char *name, Qid *qid)
+{
+ int dotdot, i;
+ vlong id;
+ char *key;
+ SmugFid *sf;
+ char *x;
+ Upload *u;
+
+ dotdot = strcmp(name, "..") == 0;
+ sf = fid->aux;
+ switch(sf->type){
+ default:
+ NotFound:
+ return "file not found";
+
+ case Qroot:
+ if(dotdot)
+ break;
+ if(strcmp(name, "ctl") == 0){
+ sf->type = Qctl;
+ break;
+ }
+ if(strcmp(name, "uploads") == 0){
+ sf->type = Quploads;
+ break;
+ }
+ if(strcmp(name, "rpclog") == 0){
+ sf->type = Qrpclog;
+ break;
+ }
+ if((i = nickindex(name)) >= 0){
+ sf->nickid = i;
+ sf->type = Qnick;
+ break;
+ }
+ goto NotFound;
+
+ case Qnick:
+ if(dotdot){
+ sf->type = Qroot;
+ sf->nickid = 0;
+ break;
+ }
+ if(strcmp(name, "ctl") == 0){
+ sf->type = Qnickctl;
+ break;
+ }
+ if(strcmp(name, "albums") == 0){
+ sf->category = -1;
+ sf->type = Qalbums;
+ break;
+ }
+ if((id = getcategoryid(sf, name)) >= 0){
+ sf->category = id;
+ sf->type = Qcategory;
+ break;
+ }
+ goto NotFound;
+
+ case Qalbums:
+ case Qcategory:
+ if(dotdot){
+ sf->category = 0;
+ sf->type = Qnick;
+ break;
+ }
+ if(strcmp(name, "ctl") == 0){
+ sf->type++;
+ break;
+ }
+ if((id = getalbumid(sf, name, &key)) >= 0){
+ sf->album = id;
+ sf->albumkey = key;
+ sf->type = Qalbum;
+ break;
+ }
+ goto NotFound;
+
+ case Qalbum:
+ if(dotdot){
+ free(sf->albumkey);
+ sf->albumkey = nil;
+ sf->album = 0;
+ if(sf->category == -1)
+ sf->type = Qalbums;
+ else
+ sf->type = Qcategory;
+ break;
+ }
+ if(strcmp(name, "ctl") == 0){
+ sf->type = Qalbumctl;
+ break;
+ }
+ if(strcmp(name, "settings") == 0){
+ sf->type = Qalbumsettings;
+ break;
+ }
+ if((id = getimageid(sf, name, &key)) >= 0){
+ sf->image = id;
+ sf->imagekey = key;
+ sf->type = Qimage;
+ break;
+ }
+ if((u = getuploadname(sf, name)) != nil){
+ sf->upload = u;
+ sf->type = Quploadfile;
+ break;
+ }
+ goto NotFound;
+
+ case Qimage:
+ if(dotdot){
+ free(sf->imagekey);
+ sf->imagekey = nil;
+ sf->image = 0;
+ sf->type = Qalbum;
+ break;
+ }
+ if(strcmp(name, "ctl") == 0){
+ sf->type = Qimagectl;
+ break;
+ }
+ if(strcmp(name, "url") == 0){
+ sf->type = Qimageurl;
+ break;
+ }
+ if(strcmp(name, "settings") == 0){
+ sf->type = Qimagesettings;
+ break;
+ }
+ if(strcmp(name, "exif") == 0){
+ sf->type = Qimageexif;
+ break;
+ }
+ x = imagename(sf);
+ if(x && strcmp(name, x) == 0){
+ free(x);
+ sf->type = Qimagefile;
+ break;
+ }
+ free(x);
+ goto NotFound;
+ }
+ dostat(sf, qid, nil);
+ fid->qid = *qid;
+ return nil;
+}
+
+static int
+dodirgen(int i, Dir *d, void *v)
+{
+ SmugFid *sf, xsf;
+ char *key;
+ vlong id;
+ Upload *u;
+
+ sf = v;
+ xsf = *sf;
+ if(i-- == 0){
+ xsf.type++; // ctl in every directory
+ dostat(&xsf, nil, d);
+ return 0;
+ }
+
+ switch(sf->type){
+ default:
+ return -1;
+
+ case Qroot:
+ if(i-- == 0){
+ xsf.type = Qrpclog;
+ dostat(&xsf, nil, d);
+ return 0;
+ }
+ if(i < 0 || i >= nnick)
+ return -1;
+ xsf.type = Qnick;
+ xsf.nickid = i;
+ dostat(&xsf, nil, d);
+ return 0;
+
+ case Qnick:
+ if(i-- == 0){
+ xsf.type = Qalbums;
+ dostat(&xsf, nil, d);
+ return 0;
+ }
+ if((id = getcategoryindex(sf, i)) < 0)
+ return -1;
+ xsf.type = Qcategory;
+ xsf.category = id;
+ dostat(&xsf, nil, d);
+ return 0;
+
+ case Qalbums:
+ case Qcategory:
+ if((id = getalbumindex(sf, i, &key)) < 0)
+ return -1;
+ xsf.type = Qalbum;
+ xsf.album = id;
+ xsf.albumkey = key;
+ dostat(&xsf, nil, d);
+ free(key);
+ return 0;
+
+ case Qalbum:
+ if(i-- == 0){
+ xsf.type = Qalbumsettings;
+ dostat(&xsf, nil, d);
+ return 0;
+ }
+ if((u = getuploadindex(sf, &i)) != nil){
+ xsf.upload = u;
+ xsf.type = Quploadfile;
+ dostat(&xsf, nil, d);
+ closeupload(u);
+ return 0;
+ }
+ if((id = getimageindex(sf, i, &key)) < 0)
+ return -1;
+ xsf.type = Qimage;
+ xsf.image = id;
+ xsf.imagekey = key;
+ dostat(&xsf, nil, d);
+ free(key);
+ return 0;
+
+ case Qimage:
+ if(i-- == 0){
+ xsf.type = Qimagefile;
+ dostat(&xsf, nil, d);
+ return 0;
+ }
+ if(i-- == 0){
+ xsf.type = Qimageexif;
+ dostat(&xsf, nil, d);
+ return 0;
+ }
+ if(i-- == 0){
+ xsf.type = Qimagesettings;
+ dostat(&xsf, nil, d);
+ return 0;
+ }
+ if(i-- == 0){
+ xsf.type = Qimageurl;
+ dostat(&xsf, nil, d);
+ return 0;
+ }
+ return -1;
+ }
+}
+
+static void
+xstat(Req *r)
+{
+ dostat(r->fid->aux, nil, &r->d);
+ respond(r, nil);
+}
+
+static void
+xwstat(Req *r)
+{
+ SmugFid *sf;
+ Json *v;
+ char *s;
+ char strid[50];
+
+ sf = r->fid->aux;
+ if(r->d.uid[0] || r->d.gid[0] || r->d.muid[0] || ~r->d.mode != 0
+ || ~r->d.atime != 0 || ~r->d.mtime != 0 || ~r->d.length != 0){
+ respond(r, "invalid wstat");
+ return;
+ }
+ if(r->d.name[0]){
+ switch(sf->type){
+ default:
+ respond(r, "invalid wstat");
+ return;
+ // TODO: rename category
+ case Qalbum:
+ snprint(strid, sizeof strid, "%lld", sf->album);
+ v = ncsmug("smugmug.albums.changeSettings",
+ "AlbumID", strid, "Title", r->d.name, nil);
+ if(v == nil)
+ responderrstr(r);
+ else
+ respond(r, nil);
+ s = smprint("&AlbumID=%lld&", sf->album);
+ jcacheflush(s);
+ free(s);
+ jcacheflush("smugmug.albums.get&");
+ return;
+ }
+ }
+ respond(r, "invalid wstat");
+}
+
+static void
+xattach(Req *r)
+{
+ SmugFid *sf;
+
+ sf = emalloc(sizeof *sf);
+ r->fid->aux = sf;
+ sf->type = Qroot;
+ dostat(sf, &r->ofcall.qid, nil);
+ r->fid->qid = r->ofcall.qid;
+ respond(r, nil);
+}
+
+void
+xopen(Req *r)
+{
+ SmugFid *sf;
+
+ if((r->ifcall.mode&~OTRUNC) > 2){
+ respond(r, "permission denied");
+ return;
+ }
+
+ sf = r->fid->aux;
+ switch(sf->type){
+ case Qctl:
+ case Qnickctl:
+ case Qalbumsctl:
+ case Qcategoryctl:
+ case Qalbumctl:
+ case Qimagectl:
+ case Qalbumsettings:
+ case Qimagesettings:
+ break;
+
+ case Quploadfile:
+ if(r->ifcall.mode != OREAD){
+ lock(&sf->upload->lk);
+ if(sf->upload->ready){
+ unlock(&sf->upload->lk);
+ respond(r, "permission denied");
+ return;
+ }
+ sf->upwriter = 1;
+ sf->upload->nwriters++;
+ unlock(&sf->upload->lk);
+ }
+ break;
+
+ default:
+ if(r->ifcall.mode != OREAD){
+ respond(r, "permission denied");
+ return;
+ }
+ break;
+ }
+
+ r->ofcall.qid = r->fid->qid;
+ respond(r, nil);
+}
+
+void
+xcreate(Req *r)
+{
+ SmugFid *sf;
+ Json *v;
+ vlong id;
+ char strid[50], *key;
+ Upload *u;
+
+ sf = r->fid->aux;
+ switch(sf->type){
+ case Qnick:
+ // Create new category.
+ if(!(r->ifcall.perm&DMDIR))
+ break;
+ v = ncsmug("smugmug.categories.create",
+ "Name", r->ifcall.name, nil);
+ if(v == nil){
+ responderrstr(r);
+ return;
+ }
+ id = jint(jwalk(v, "Category/id"));
+ if(id < 0){
+ fprint(2, "Create category: %J\n", v);
+ jclose(v);
+ responderrstr(r);
+ return;
+ }
+ sf->type = Qcategory;
+ sf->category = id;
+ jcacheflush("method=smugmug.users.getTree&");
+ jcacheflush("method=smugmug.categories.get&");
+ dostat(sf, &r->ofcall.qid, nil);
+ respond(r, nil);
+ return;
+
+ case Qcategory:
+ // Create new album.
+ if(!(r->ifcall.perm&DMDIR))
+ break;
+ snprint(strid, sizeof strid, "%lld", sf->category);
+ // Start with most restrictive settings.
+ v = ncsmug("smugmug.albums.create",
+ "Title", r->ifcall.name,
+ "CategoryID", strid,
+ "Public", "0",
+ "WorldSearchable", "0",
+ "SmugSearchable", "0",
+ nil);
+ if(v == nil){
+ responderrstr(r);
+ return;
+ }
+ id = jint(jwalk(v, "Album/id"));
+ key = jstring(jwalk(v, "Album/Key"));
+ if(id < 0 || key == nil){
+ fprint(2, "Create album: %J\n", v);
+ jclose(v);
+ responderrstr(r);
+ return;
+ }
+ sf->type = Qalbum;
+ sf->album = id;
+ sf->albumkey = estrdup(key);
+ jclose(v);
+ jcacheflush("method=smugmug.users.getTree&");
+ dostat(sf, &r->ofcall.qid, nil);
+ respond(r, nil);
+ return;
+
+ case Qalbum:
+ // Upload image to album.
+ if(r->ifcall.perm&DMDIR)
+ break;
+ u = newupload(sf, r->ifcall.name);
+ if(u == nil){
+ responderrstr(r);
+ return;
+ }
+ sf->upload = u;
+ sf->upwriter = 1;
+ sf->type = Quploadfile;
+ dostat(sf, &r->ofcall.qid, nil);
+ respond(r, nil);
+ return;
+ }
+ respond(r, "permission denied");
+}
+
+static int
+writetofd(Req *r, int fd)
+{
+ int total, n;
+
+ total = 0;
+ while(total < r->ifcall.count){
+ n = pwrite(fd, (char*)r->ifcall.data+total, r->ifcall.count-total, r->ifcall.offset+total);
+ if(n <= 0)
+ return -1;
+ total += n;
+ }
+ r->ofcall.count = r->ifcall.count;
+ return 0;
+}
+
+static void
+readfromfd(Req *r, int fd)
+{
+ int n;
+ n = pread(fd, r->ofcall.data, r->ifcall.count, r->ifcall.offset);
+ if(n < 0)
+ n = 0;
+ r->ofcall.count = n;
+}
+
+void
+xread(Req *r)
+{
+ SmugFid *sf;
+ char *data;
+ int fd;
+ HTTPHeader hdr;
+ char *url;
+
+ sf = r->fid->aux;
+ r->ofcall.count = 0;
+ switch(sf->type){
+ default:
+ respond(r, "not implemented");
+ return;
+ case Qroot:
+ case Qnick:
+ case Qalbums:
+ case Qcategory:
+ case Qalbum:
+ case Qimage:
+ dirread9p(r, dodirgen, sf);
+ break;
+ case Qrpclog:
+ rpclogread(r);
+ return;
+ case Qctl:
+ case Qnickctl:
+ case Qalbumsctl:
+ case Qcategoryctl:
+ case Qalbumctl:
+ case Qimagectl:
+ case Qimageurl:
+ case Qimageexif:
+ case Quploads:
+ case Qimagesettings:
+ case Qalbumsettings:
+ data = readctl(sf);
+ readstr(r, data);
+ free(data);
+ break;
+ case Qimagefile:
+ url = imageurl(sf);
+ if(url == nil || (fd = download(url, &hdr)) < 0){
+ free(url);
+ responderrstr(r);
+ return;
+ }
+ readfromfd(r, fd);
+ free(url);
+ close(fd);
+ break;
+ case Quploadfile:
+ if(sf->upload)
+ readfromfd(r, sf->upload->fd);
+ break;
+ }
+ respond(r, nil);
+}
+
+void
+xwrite(Req *r)
+{
+ int sync;
+ char *s, *t, *p;
+ Json *v;
+ char strid[50];
+ SmugFid *sf;
+
+ sf = r->fid->aux;
+ r->ofcall.count = r->ifcall.count;
+ sync = (r->ifcall.count==4 && memcmp(r->ifcall.data, "sync", 4) == 0);
+ switch(sf->type){
+ case Qctl:
+ if(sync){
+ jcacheflush(nil);
+ respond(r, nil);
+ return;
+ }
+ break;
+ case Qnickctl:
+ if(sync){
+ s = smprint("&NickName=%s&", nickname(sf->nickid));
+ jcacheflush(s);
+ free(s);
+ respond(r, nil);
+ return;
+ }
+ break;
+ case Qalbumsctl:
+ case Qcategoryctl:
+ jcacheflush("smugmug.categories.get");
+ break;
+ case Qalbumctl:
+ if(sync){
+ s = smprint("&AlbumID=%lld&", sf->album);
+ jcacheflush(s);
+ free(s);
+ respond(r, nil);
+ return;
+ }
+ break;
+ case Qimagectl:
+ if(sync){
+ s = smprint("&ImageID=%lld&", sf->image);
+ jcacheflush(s);
+ free(s);
+ respond(r, nil);
+ return;
+ }
+ break;
+ case Quploadfile:
+ if(sf->upload){
+ if(writetofd(r, sf->upload->fd) < 0){
+ responderrstr(r);
+ return;
+ }
+ respond(r, nil);
+ return;
+ }
+ break;
+ case Qimagesettings:
+ case Qalbumsettings:
+ s = (char*)r->ifcall.data; // lib9p nul-terminated it
+ t = strpbrk(s, " \r\t\n");
+ if(t == nil)
+ t = "";
+ else{
+ *t++ = 0;
+ while(*t == ' ' || *t == '\r' || *t == '\t' || *t == '\n')
+ t++;
+ }
+ p = strchr(t, '\n');
+ if(p && p[1] == 0)
+ *p = 0;
+ else if(p){
+ respond(r, "newline in argument");
+ return;
+ }
+ if(sf->type == Qalbumsettings)
+ goto Albumsettings;
+ snprint(strid, sizeof strid, "%lld", sf->image);
+ v = ncsmug("smugmug.images.changeSettings",
+ "ImageID", strid,
+ s, t, nil);
+ if(v == nil)
+ responderrstr(r);
+ else
+ respond(r, nil);
+ s = smprint("&ImageID=%lld&", sf->image);
+ jcacheflush(s);
+ free(s);
+ return;
+ Albumsettings:
+ snprint(strid, sizeof strid, "%lld", sf->album);
+ v = ncsmug("smugmug.albums.changeSettings",
+ "AlbumID", strid, s, t, nil);
+ if(v == nil)
+ responderrstr(r);
+ else
+ respond(r, nil);
+ s = smprint("&AlbumID=%lld&", sf->album);
+ jcacheflush(s);
+ free(s);
+ return;
+ }
+ respond(r, "invalid control message");
+ return;
+}
+
+void
+xremove(Req *r)
+{
+ char id[100];
+ SmugFid *sf;
+ Json *v;
+
+ sf = r->fid->aux;
+ switch(sf->type){
+ default:
+ respond(r, "permission denied");
+ return;
+ case Qcategoryctl:
+ case Qalbumctl:
+ case Qalbumsettings:
+ case Qimagectl:
+ case Qimagesettings:
+ case Qimageexif:
+ case Qimageurl:
+ case Qimagefile:
+ /* ignore remove request, but no error, so rm -r works */
+ /* you can pretend they get removed and immediately grow back! */
+ respond(r, nil);
+ return;
+ case Qcategory:
+ v = getalbums(sf);
+ if(v && v->len > 0){
+ respond(r, "directory not empty");
+ return;
+ }
+ snprint(id, sizeof id, "%lld", sf->category);
+ v = ncsmug("smugmug.categories.delete",
+ "CategoryID", id, nil);
+ if(v == nil)
+ responderrstr(r);
+ else{
+ jclose(v);
+ jcacheflush("smugmug.users.getTree");
+ jcacheflush("smugmug.categories.get");
+ respond(r, nil);
+ }
+ return;
+ case Qalbum:
+ v = getimages(sf, sf->album, sf->albumkey);
+ if(v && v->len > 0){
+ respond(r, "directory not empty");
+ return;
+ }
+ snprint(id, sizeof id, "%lld", sf->album);
+ v = ncsmug("smugmug.albums.delete",
+ "AlbumID", id, nil);
+ if(v == nil)
+ responderrstr(r);
+ else{
+ jclose(v);
+ jcacheflush("smugmug.users.getTree");
+ jcacheflush("smugmug.categories.get");
+ jcacheflush("smugmug.albums.get");
+ respond(r, nil);
+ }
+ return;
+
+ case Qimage:
+ snprint(id, sizeof id, "%lld", sf->image);
+ v = ncsmug("smugmug.images.delete",
+ "ImageID", id, nil);
+ if(v == nil)
+ responderrstr(r);
+ else{
+ jclose(v);
+ snprint(id, sizeof id, "ImageID=%lld&", sf->image);
+ jcacheflush(id);
+ jcacheflush("smugmug.images.get&");
+ respond(r, nil);
+ }
+ return;
+ }
+}
+
+void
+xflush(Req *r)
+{
+ rpclogflush(r->oldreq);
+ respond(r, nil);
+}
+
+Srv xsrv;
+
+void
+xinit(void)
+{
+ xsrv.attach = xattach;
+ xsrv.open = xopen;
+ xsrv.create = xcreate;
+ xsrv.read = xread;
+ xsrv.stat = xstat;
+ xsrv.walk1 = xwalk1;
+ xsrv.clone = xclone;
+ xsrv.destroyfid = xdestroyfid;
+ xsrv.remove = xremove;
+ xsrv.write = xwrite;
+ xsrv.flush = xflush;
+ xsrv.wstat = xwstat;
+}
diff --git a/src/cmd/smugfs/http.c b/src/cmd/smugfs/http.c
new file mode 100644
index 00000000..9cf7f1d0
--- /dev/null
+++ b/src/cmd/smugfs/http.c
@@ -0,0 +1,237 @@
+#include "a.h"
+
+static char*
+haveheader(char *buf, int n)
+{
+ int i;
+
+ for(i=0; i<n; i++){
+ if(buf[i] == '\n'){
+ if(i+2 < n && buf[i+1] == '\r' && buf[i+2] == '\n')
+ return buf+i+3;
+ if(i+1 < n && buf[i+1] == '\n')
+ return buf+i+2;
+ }
+ }
+ return 0;
+}
+
+static int
+parseheader(char *buf, int n, HTTPHeader *hdr)
+{
+ int nline;
+ char *data, *ebuf, *p, *q, *next;
+
+ memset(hdr, 0, sizeof *hdr);
+ ebuf = buf+n;
+ data = haveheader(buf, n);
+ if(data == nil)
+ return -1;
+
+ data[-1] = 0;
+ if(data[-2] == '\r')
+ data[-2] = 0;
+ if(chattyhttp > 1){
+ fprint(2, "--HTTP Response Header:\n");
+ fprint(2, "%s\n", buf);
+ fprint(2, "--\n");
+ }
+ nline = 0;
+ for(p=buf; *p; p=next, nline++){
+ q = strchr(p, '\n');
+ if(q){
+ next = q+1;
+ *q = 0;
+ if(q > p && q[-1] == '\r')
+ q[-1] = 0;
+ }else
+ next = p+strlen(p);
+ if(nline == 0){
+ if(memcmp(p, "HTTP/", 5) != 0){
+ werrstr("invalid HTTP version: %.10s", p);
+ return -1;
+ }
+ q = strchr(p, ' ');
+ if(q == nil){
+ werrstr("invalid HTTP version");
+ return -1;
+ }
+ *q++ = 0;
+ strncpy(hdr->proto, p, sizeof hdr->proto);
+ hdr->proto[sizeof hdr->proto-1] = 0;
+ while(*q == ' ')
+ q++;
+ if(*q < '0' || '9' < *q){
+ werrstr("invalid HTTP response code");
+ return -1;
+ }
+ p = q;
+ q = strchr(p, ' ');
+ if(q == nil)
+ q = p+strlen(p);
+ else
+ *q++ = 0;
+ hdr->code = strtol(p, &p, 10);
+ if(*p != 0)
+ return -1;
+ while(*q == ' ')
+ q++;
+ strncpy(hdr->codedesc, q, sizeof hdr->codedesc);
+ hdr->codedesc[sizeof hdr->codedesc-1] = 0;
+ continue;
+ }
+ q = strchr(p, ':');
+ if(q == nil)
+ continue;
+ *q++ = 0;
+ while(*q != 0 && (*q == ' ' || *q == '\t'))
+ q++;
+ if(cistrcmp(p, "Content-Type") == 0){
+ strncpy(hdr->contenttype, q, sizeof hdr->contenttype);
+ hdr->contenttype[sizeof hdr->contenttype-1] = 0;
+ continue;
+ }
+ if(cistrcmp(p, "Content-Length") == 0 && '0' <= *q && *q <= '9'){
+ hdr->contentlength = strtoll(q, 0, 10);
+ continue;
+ }
+ }
+ if(nline < 1){
+ werrstr("no header");
+ return -1;
+ }
+
+ memmove(buf, data, ebuf - data);
+ return ebuf - data;
+}
+
+static char*
+genhttp(Protocol *proto, char *host, char *req, HTTPHeader *hdr, int wfd, int rfd, vlong rtotal)
+{
+ int n, m, total, want;
+ char buf[8192], *data;
+ Pfd *fd;
+
+ if(chattyhttp > 1){
+ fprint(2, "--HTTP Request:\n");
+ fprint(2, "%s", req);
+ fprint(2, "--\n");
+ }
+ fd = proto->connect(host);
+ if(fd == nil){
+ if(chattyhttp > 0)
+ fprint(2, "connect %s: %r\n", host);
+ return nil;
+ }
+
+ n = strlen(req);
+ if(proto->write(fd, req, n) != n){
+ if(chattyhttp > 0)
+ fprint(2, "write %s: %r\n", host);
+ proto->close(fd);
+ return nil;
+ }
+
+ if(rfd >= 0){
+ while(rtotal > 0){
+ m = sizeof buf;
+ if(m > rtotal)
+ m = rtotal;
+ if((n = read(rfd, buf, m)) <= 0){
+ fprint(2, "read: missing data\n");
+ proto->close(fd);
+ return nil;
+ }
+ if(proto->write(fd, buf, n) != n){
+ fprint(2, "write data: %r\n");
+ proto->close(fd);
+ return nil;
+ }
+ rtotal -= n;
+ }
+ }
+
+ total = 0;
+ while(!haveheader(buf, total)){
+ n = proto->read(fd, buf+total, sizeof buf-total);
+ if(n <= 0){
+ if(chattyhttp > 0)
+ fprint(2, "read missing header\n");
+ proto->close(fd);
+ return nil;
+ }
+ total += n;
+ }
+
+ n = parseheader(buf, total, hdr);
+ if(n < 0){
+ fprint(2, "failed response parse: %r\n");
+ proto->close(fd);
+ return nil;
+ }
+ if(hdr->contentlength >= MaxResponse){
+ werrstr("response too long");
+ proto->close(fd);
+ return nil;
+ }
+ if(hdr->contentlength >= 0 && n > hdr->contentlength)
+ n = hdr->contentlength;
+ want = sizeof buf;
+ data = nil;
+ total = 0;
+ goto didread;
+
+ while(want > 0 && (n = proto->read(fd, buf, want)) > 0){
+ didread:
+ if(wfd >= 0){
+ if(writen(wfd, buf, n) < 0){
+ proto->close(fd);
+ werrstr("write error");
+ return nil;
+ }
+ }else{
+ data = erealloc(data, total+n);
+ memmove(data+total, buf, n);
+ }
+ total += n;
+ if(total > MaxResponse){
+ proto->close(fd);
+ werrstr("response too long");
+ return nil;
+ }
+ if(hdr->contentlength >= 0 && total + want > hdr->contentlength)
+ want = hdr->contentlength - total;
+ }
+ proto->close(fd);
+
+ if(hdr->contentlength >= 0 && total != hdr->contentlength){
+ werrstr("got wrong content size %d %d", total, hdr->contentlength);
+ return nil;
+ }
+ hdr->contentlength = total;
+ if(wfd >= 0)
+ return (void*)1;
+ else{
+ data = erealloc(data, total+1);
+ data[total] = 0;
+ }
+ return data;
+}
+
+char*
+httpreq(Protocol *proto, char *host, char *req, HTTPHeader *hdr, int rfd, vlong rlength)
+{
+ return genhttp(proto, host, req, hdr, -1, rfd, rlength);
+}
+
+int
+httptofile(Protocol *proto, char *host, char *req, HTTPHeader *hdr, int fd)
+{
+ if(fd < 0){
+ werrstr("bad fd");
+ return -1;
+ }
+ if(genhttp(proto, host, req, hdr, fd, -1, 0) == nil)
+ return -1;
+ return 0;
+}
diff --git a/src/cmd/smugfs/icache.c b/src/cmd/smugfs/icache.c
new file mode 100644
index 00000000..750f232c
--- /dev/null
+++ b/src/cmd/smugfs/icache.c
@@ -0,0 +1,171 @@
+#include "a.h"
+
+// This code is almost certainly wrong.
+
+typedef struct Icache Icache;
+struct Icache
+{
+ char *url;
+ HTTPHeader hdr;
+ char *tmpfile;
+ int fd;
+ Icache *next;
+ Icache *prev;
+ Icache *hash;
+};
+
+enum {
+ NHASH = 128,
+ MAXCACHE = 128,
+};
+static struct {
+ Icache *hash[NHASH];
+ Icache *head;
+ Icache *tail;
+ int n;
+} icache;
+
+static Icache*
+icachefind(char *url)
+{
+ int h;
+ Icache *ic;
+
+ h = hash(url) % NHASH;
+ for(ic=icache.hash[h]; ic; ic=ic->hash){
+ if(strcmp(ic->url, url) == 0){
+ /* move to front */
+ if(ic->prev) {
+ ic->prev->next = ic->next;
+ if(ic->next)
+ ic->next->prev = ic->prev;
+ else
+ icache.tail = ic->prev;
+ ic->prev = nil;
+ ic->next = icache.head;
+ icache.head->prev = ic;
+ icache.head = ic;
+ }
+ return ic;
+ }
+ }
+ return nil;
+}
+
+static Icache*
+icacheinsert(char *url, HTTPHeader *hdr, char *file, int fd)
+{
+ int h;
+ Icache *ic, **l;
+
+ if(icache.n == MAXCACHE){
+ ic = icache.tail;
+ icache.tail = ic->prev;
+ if(ic->prev)
+ ic->prev->next = nil;
+ else
+ icache.head = ic->prev;
+ h = hash(ic->url) % NHASH;
+ for(l=&icache.hash[h]; *l; l=&(*l)->hash){
+ if(*l == ic){
+ *l = ic->hash;
+ goto removed;
+ }
+ }
+ sysfatal("cannot find ic in cache");
+ removed:
+ free(ic->url);
+ close(ic->fd);
+ remove(ic->file);
+ free(ic->file);
+ }else{
+ ic = emalloc(sizeof *ic);
+ icache.n++;
+ }
+
+ ic->url = estrdup(url);
+ ic->fd = dup(fd, -1);
+ ic->file = estrdup(file);
+ ic->hdr = *hdr;
+ h = hash(url) % NHASH;
+ ic->hash = icache.hash[h];
+ icache.hash[h] = ic;
+ ic->prev = nil;
+ ic->next = icache.head;
+ if(ic->next)
+ ic->next->prev = ic;
+ else
+ icache.tail = ic;
+ return ic;
+}
+
+void
+icacheflush(char *substr)
+{
+ Icache **l, *ic;
+
+ for(l=&icache.head; (ic=*l); ) {
+ if(substr == nil || strstr(ic->url, substr)) {
+ icache.n--;
+ *l = ic->next;
+ free(ic->url);
+ close(ic->fd);
+ remove(ic->file);
+ free(ic->file);
+ free(ic);
+ }else
+ l = &ic->next;
+ }
+
+ if(icache.head) {
+ icache.head->prev = nil;
+ for(ic=icache.head; ic; ic=ic->next){
+ if(ic->next)
+ ic->next->prev = ic;
+ else
+ icache.tail = ic;
+ }
+ }else
+ icache.tail = nil;
+}
+
+int
+urlfetch(char *url, HTTPHeader hdr)
+{
+ Icache *ic;
+ char buf[50], *host, *path, *p;
+ int fd, len;
+
+ ic = icachefind(url);
+ if(ic != nil){
+ *hdr = ic->hdr;
+ return dup(ic->fd, -1);
+ }
+
+ if(memcmp(url, "http://", 7) != 0){
+ werrstr("non-http url");
+ return -1;
+ }
+ p = strchr(url+7, '/');
+ if(p == nil)
+ p = url+strlen(url);
+ len = p - (url+7);
+ host = emalloc(len+1);
+ memmove(host, url+7, len);
+ host[len] = 0;
+ if(*p == 0)
+ p = "/";
+
+ strcpy(buf, "/var/tmp/smugfs.XXXXXX");
+ fd = opentemp(buf, ORDWR|ORCLOSE);
+ if(fd < 0)
+ return -1;
+ if(httptofile(http, host, req, &hdr, fd) < 0){
+ free(host);
+ return -1;
+ }
+ free(host);
+ icacheinsert(url, &hdr, buf, fd);
+ return fd;
+}
+
diff --git a/src/cmd/smugfs/json.c b/src/cmd/smugfs/json.c
new file mode 100644
index 00000000..d6472b4d
--- /dev/null
+++ b/src/cmd/smugfs/json.c
@@ -0,0 +1,555 @@
+#include "a.h"
+
+static Json *parsevalue(char**);
+
+static char*
+wskip(char *p)
+{
+ while(*p == ' ' || *p == '\t' || *p == '\n' || *p == '\v')
+ p++;
+ return p;
+}
+
+static int
+ishex(int c)
+{
+ return '0' <= c && c <= '9' ||
+ 'a' <= c && c <= 'f' ||
+ 'A' <= c && c <= 'F';
+}
+
+static Json*
+newjval(int type)
+{
+ Json *v;
+
+ v = emalloc(sizeof *v);
+ v->ref = 1;
+ v->type = type;
+ return v;
+}
+
+static Json*
+badjval(char **pp, char *fmt, ...)
+{
+ char buf[ERRMAX];
+ va_list arg;
+
+ if(fmt){
+ va_start(arg, fmt);
+ vsnprint(buf, sizeof buf, fmt, arg);
+ va_end(arg);
+ errstr(buf, sizeof buf);
+ }
+ *pp = nil;
+ return nil;
+}
+
+static char*
+_parsestring(char **pp, int *len)
+{
+ char *p, *q, *w, *s, *r;
+ char buf[5];
+ Rune rune;
+
+ p = wskip(*pp);
+ if(*p != '"'){
+ badjval(pp, "missing opening quote for string");
+ return nil;
+ }
+ for(q=p+1; *q && *q != '\"'; q++){
+ if(*q == '\\' && *(q+1) != 0)
+ q++;
+ if((*q & 0xFF) < 0x20){ // no control chars
+ badjval(pp, "control char in string");
+ return nil;
+ }
+ }
+ if(*q == 0){
+ badjval(pp, "no closing quote in string");
+ return nil;
+ }
+ s = emalloc(q - p);
+ w = s;
+ for(r=p+1; r<q; ){
+ if(*r != '\\'){
+ *w++ = *r++;
+ continue;
+ }
+ r++;
+ switch(*r){
+ default:
+ free(s);
+ badjval(pp, "bad escape \\%c in string", *r&0xFF);
+ return nil;
+ case '\\':
+ case '\"':
+ case '/':
+ *w++ = *r++;
+ break;
+ case 'b':
+ *w++ = '\b';
+ r++;
+ break;
+ case 'f':
+ *w++ = '\f';
+ r++;
+ break;
+ case 'n':
+ *w++ = '\n';
+ r++;
+ break;
+ case 'r':
+ *w++ = '\r';
+ r++;
+ break;
+ case 't':
+ *w++ = '\t';
+ r++;
+ break;
+ case 'u':
+ r++;
+ if(!ishex(r[0]) || !ishex(r[1]) || !ishex(r[2]) || !ishex(r[3])){
+ free(s);
+ badjval(pp, "bad hex \\u%.4s", r);
+ return nil;
+ }
+ memmove(buf, r, 4);
+ buf[4] = 0;
+ rune = strtol(buf, 0, 16);
+ if(rune == 0){
+ free(s);
+ badjval(pp, "\\u0000 in string");
+ return nil;
+ }
+ r += 4;
+ w += runetochar(w, &rune);
+ break;
+ }
+ }
+ *w = 0;
+ if(len)
+ *len = w - s;
+ *pp = q+1;
+ return s;
+}
+
+static Json*
+parsenumber(char **pp)
+{
+ char *p, *q;
+ char *t;
+ double d;
+ Json *v;
+
+ /* -?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?([Ee][-+]?[0-9]+) */
+ p = wskip(*pp);
+ q = p;
+ if(*q == '-')
+ q++;
+ if(*q == '0')
+ q++;
+ else{
+ if(*q < '1' || *q > '9')
+ return badjval(pp, "invalid number");
+ while('0' <= *q && *q <= '9')
+ q++;
+ }
+ if(*q == '.'){
+ q++;
+ if(*q < '0' || *q > '9')
+ return badjval(pp, "invalid number");
+ while('0' <= *q && *q <= '9')
+ q++;
+ }
+ if(*q == 'e' || *q == 'E'){
+ q++;
+ if(*q == '-' || *q == '+')
+ q++;
+ if(*q < '0' || *q > '9')
+ return badjval(pp, "invalid number");
+ while('0' <= *q && *q <= '9')
+ q++;
+ }
+
+ t = emalloc(q-p+1);
+ memmove(t, p, q-p);
+ t[q-p] = 0;
+ errno = 0;
+ d = strtod(t, nil);
+ if(errno != 0){
+ free(t);
+ return badjval(pp, nil);
+ }
+ free(t);
+ v = newjval(Jnumber);
+ v->number = d;
+ *pp = q;
+ return v;
+}
+
+static Json*
+parsestring(char **pp)
+{
+ char *s;
+ Json *v;
+ int len;
+
+ s = _parsestring(pp, &len);
+ if(s == nil)
+ return nil;
+ v = newjval(Jstring);
+ v->string = s;
+ v->len = len;
+ return v;
+}
+
+static Json*
+parsename(char **pp)
+{
+ if(strncmp(*pp, "true", 4) == 0){
+ *pp += 4;
+ return newjval(Jtrue);
+ }
+ if(strncmp(*pp, "false", 5) == 0){
+ *pp += 5;
+ return newjval(Jfalse);
+ }
+ if(strncmp(*pp, "null", 4) == 0){
+ *pp += 4;
+ return newjval(Jtrue);
+ }
+ return badjval(pp, "invalid name");
+}
+
+static Json*
+parsearray(char **pp)
+{
+ char *p;
+ Json *v;
+
+ p = *pp;
+ if(*p++ != '[')
+ return badjval(pp, "missing bracket for array");
+ v = newjval(Jarray);
+ p = wskip(p);
+ if(*p != ']'){
+ for(;;){
+ if(v->len%32 == 0)
+ v->value = erealloc(v->value, (v->len+32)*sizeof v->value[0]);
+ if((v->value[v->len++] = parsevalue(&p)) == nil){
+ jclose(v);
+ return badjval(pp, nil);
+ }
+ p = wskip(p);
+ if(*p == ']')
+ break;
+ if(*p++ != ','){
+ jclose(v);
+ return badjval(pp, "missing comma in array");
+ }
+ }
+ }
+ p++;
+ *pp = p;
+ return v;
+}
+
+static Json*
+parseobject(char **pp)
+{
+ char *p;
+ Json *v;
+
+ p = *pp;
+ if(*p++ != '{')
+ return badjval(pp, "missing brace for object");
+ v = newjval(Jobject);
+ p = wskip(p);
+ if(*p != '}'){
+ for(;;){
+ if(v->len%32 == 0){
+ v->name = erealloc(v->name, (v->len+32)*sizeof v->name[0]);
+ v->value = erealloc(v->value, (v->len+32)*sizeof v->value[0]);
+ }
+ if((v->name[v->len++] = _parsestring(&p, nil)) == nil){
+ jclose(v);
+ return badjval(pp, nil);
+ }
+ p = wskip(p);
+ if(*p++ != ':'){
+ jclose(v);
+ return badjval(pp, "missing colon in object");
+ }
+ if((v->value[v->len-1] = parsevalue(&p)) == nil){
+ jclose(v);
+ return badjval(pp, nil);
+ }
+ p = wskip(p);
+ if(*p == '}')
+ break;
+ if(*p++ != ','){
+ jclose(v);
+ return badjval(pp, "missing comma in object");
+ }
+ }
+ }
+ p++;
+ *pp = p;
+ return v;
+}
+
+static Json*
+parsevalue(char **pp)
+{
+ *pp = wskip(*pp);
+ switch(**pp){
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case '-':
+ return parsenumber(pp);
+ case 't':
+ case 'f':
+ case 'n':
+ return parsename(pp);
+ case '\"':
+ return parsestring(pp);
+ case '[':
+ return parsearray(pp);
+ case '{':
+ return parseobject(pp);
+ default:
+ return badjval(pp, "unexpected char <%02x>", **pp & 0xFF);
+ }
+}
+
+Json*
+parsejson(char *text)
+{
+ Json *v;
+
+ v = parsevalue(&text);
+ if(v && text && *wskip(text) != 0){
+ jclose(v);
+ werrstr("extra data in json");
+ return nil;
+ }
+ return v;
+}
+
+void
+_printjval(Fmt *fmt, Json *v, int n)
+{
+ int i;
+
+ if(v == nil){
+ fmtprint(fmt, "nil");
+ return;
+ }
+ switch(v->type){
+ case Jstring:
+ fmtprint(fmt, "\"%s\"", v->string);
+ break;
+ case Jnumber:
+ if(floor(v->number) == v->number)
+ fmtprint(fmt, "%.0f", v->number);
+ else
+ fmtprint(fmt, "%g", v->number);
+ break;
+ case Jobject:
+ fmtprint(fmt, "{");
+ if(n >= 0)
+ n++;
+ for(i=0; i<v->len; i++){
+ if(n > 0)
+ fmtprint(fmt, "\n%*s", n*4, "");
+ fmtprint(fmt, "\"%s\" : ", v->name[i]);
+ _printjval(fmt, v->value[i], n);
+ fmtprint(fmt, ",");
+ }
+ if(n > 0){
+ n--;
+ if(v->len > 0)
+ fmtprint(fmt, "\n%*s", n*4);
+ }
+ fmtprint(fmt, "}");
+ break;
+ case Jarray:
+ fmtprint(fmt, "[");
+ if(n >= 0)
+ n++;
+ for(i=0; i<v->len; i++){
+ if(n > 0)
+ fmtprint(fmt, "\n%*s", n*4, "");
+ _printjval(fmt, v->value[i], n);
+ fmtprint(fmt, ",");
+ }
+ if(n > 0){
+ n--;
+ if(v->len > 0)
+ fmtprint(fmt, "\n%*s", n*4);
+ }
+ fmtprint(fmt, "]");
+ break;
+ case Jtrue:
+ fmtprint(fmt, "true");
+ break;
+ case Jfalse:
+ fmtprint(fmt, "false");
+ break;
+ case Jnull:
+ fmtprint(fmt, "null");
+ break;
+ }
+}
+
+/*
+void
+printjval(Json *v)
+{
+ Fmt fmt;
+ char buf[256];
+
+ fmtfdinit(&fmt, 1, buf, sizeof buf);
+ _printjval(&fmt, v, 0);
+ fmtprint(&fmt, "\n");
+ fmtfdflush(&fmt);
+}
+*/
+
+int
+jsonfmt(Fmt *fmt)
+{
+ Json *v;
+
+ v = va_arg(fmt->args, Json*);
+ if(fmt->flags&FmtSharp)
+ _printjval(fmt, v, 0);
+ else
+ _printjval(fmt, v, -1);
+ return 0;
+}
+
+Json*
+jincref(Json *v)
+{
+ if(v == nil)
+ return nil;
+ ++v->ref;
+ return v;
+}
+
+void
+jclose(Json *v)
+{
+ int i;
+
+ if(v == nil)
+ return;
+ if(--v->ref > 0)
+ return;
+ if(v->ref < 0)
+ sysfatal("jclose: ref %d", v->ref);
+
+ switch(v->type){
+ case Jstring:
+ free(v->string);
+ break;
+ case Jarray:
+ for(i=0; i<v->len; i++)
+ jclose(v->value[i]);
+ free(v->value);
+ break;
+ case Jobject:
+ for(i=0; i<v->len; i++){
+ free(v->name[i]);
+ jclose(v->value[i]);
+ }
+ free(v->value);
+ free(v->name);
+ break;
+ }
+ free(v);
+}
+
+Json*
+jlookup(Json *v, char *name)
+{
+ int i;
+
+ if(v->type != Jobject)
+ return nil;
+ for(i=0; i<v->len; i++)
+ if(strcmp(v->name[i], name) == 0)
+ return v->value[i];
+ return nil;
+}
+
+Json*
+jwalk(Json *v, char *path)
+{
+ char elem[128], *p, *next;
+ int n;
+
+ for(p=path; *p && v; p=next){
+ next = strchr(p, '/');
+ if(next == nil)
+ next = p+strlen(p);
+ if(next-p >= sizeof elem)
+ sysfatal("jwalk path elem too long - %s", path);
+ memmove(elem, p, next-p);
+ elem[next-p] = 0;
+ if(*next == '/')
+ next++;
+ if(v->type == Jarray && *elem && (n=strtol(elem, &p, 10)) >= 0 && *p == 0){
+ if(n >= v->len)
+ return nil;
+ v = v->value[n];
+ }else
+ v = jlookup(v, elem);
+ }
+ return v;
+}
+
+char*
+jstring(Json *jv)
+{
+ if(jv == nil || jv->type != Jstring)
+ return nil;
+ return jv->string;
+}
+
+vlong
+jint(Json *jv)
+{
+ if(jv == nil || jv->type != Jnumber)
+ return -1;
+ return jv->number;
+}
+
+double
+jnumber(Json *jv)
+{
+ if(jv == nil || jv->type != Jnumber)
+ return 0;
+ return jv->number;
+}
+
+int
+jstrcmp(Json *jv, char *s)
+{
+ char *t;
+
+ t = jstring(jv);
+ if(t == nil)
+ return -2;
+ return strcmp(t, s);
+}
diff --git a/src/cmd/smugfs/jsonrpc.c b/src/cmd/smugfs/jsonrpc.c
new file mode 100644
index 00000000..92490e77
--- /dev/null
+++ b/src/cmd/smugfs/jsonrpc.c
@@ -0,0 +1,244 @@
+#include "a.h"
+
+// JSON request/reply cache.
+
+int chattyhttp;
+
+typedef struct JEntry JEntry;
+struct JEntry
+{
+ CEntry ce;
+ Json *reply;
+};
+
+static Cache *jsoncache;
+
+static void
+jfree(CEntry *ce)
+{
+ JEntry *j;
+
+ j = (JEntry*)ce;
+ jclose(j->reply);
+}
+
+static JEntry*
+jcachelookup(char *request)
+{
+ if(jsoncache == nil)
+ jsoncache = newcache(sizeof(JEntry), 1000, jfree);
+ return (JEntry*)cachelookup(jsoncache, request, 1);
+}
+
+void
+jcacheflush(char *substr)
+{
+ if(jsoncache == nil)
+ return;
+ cacheflush(jsoncache, substr);
+}
+
+
+// JSON RPC over HTTP
+
+static char*
+makehttprequest(char *host, char *path, char *postdata)
+{
+ Fmt fmt;
+
+ fmtstrinit(&fmt);
+ fmtprint(&fmt, "POST %s HTTP/1.0\r\n", path);
+ fmtprint(&fmt, "Host: %s\r\n", host);
+ fmtprint(&fmt, "User-Agent: " USER_AGENT "\r\n");
+ fmtprint(&fmt, "Content-Type: application/x-www-form-urlencoded\r\n");
+ fmtprint(&fmt, "Content-Length: %d\r\n", strlen(postdata));
+ fmtprint(&fmt, "\r\n");
+ fmtprint(&fmt, "%s", postdata);
+ return fmtstrflush(&fmt);
+}
+
+static char*
+makerequest(char *method, char *name1, va_list arg)
+{
+ char *p, *key, *val;
+ Fmt fmt;
+
+ fmtstrinit(&fmt);
+ fmtprint(&fmt, "&");
+ p = name1;
+ while(p != nil){
+ key = p;
+ val = va_arg(arg, char*);
+ if(val == nil)
+ sysfatal("jsonrpc: nil value");
+ fmtprint(&fmt, "%U=%U&", key, val);
+ p = va_arg(arg, char*);
+ }
+ // TODO: These are SmugMug-specific, probably.
+ fmtprint(&fmt, "method=%s&", method);
+ if(sessid)
+ fmtprint(&fmt, "SessionID=%s&", sessid);
+ fmtprint(&fmt, "APIKey=%s", APIKEY);
+ return fmtstrflush(&fmt);
+}
+
+static char*
+dojsonhttp(Protocol *proto, char *host, char *request, int rfd, vlong rlength)
+{
+ char *data;
+ HTTPHeader hdr;
+
+ data = httpreq(proto, host, request, &hdr, rfd, rlength);
+ if(data == nil){
+ fprint(2, "httpreq: %r\n");
+ return nil;
+ }
+ if(strcmp(hdr.contenttype, "application/json") != 0 &&
+ (strcmp(hdr.contenttype, "text/html; charset=utf-8") != 0 || data[0] != '{')){ // upload.smugmug.com, sigh
+ werrstr("bad content type: %s", hdr.contenttype);
+ fprint(2, "Content-Type: %s\n", hdr.contenttype);
+ write(2, data, hdr.contentlength);
+ return nil;
+ }
+ if(hdr.contentlength == 0){
+ werrstr("no content");
+ return nil;
+ }
+ return data;
+}
+
+Json*
+jsonrpc(Protocol *proto, char *host, char *path, char *method, char *name1, va_list arg, int usecache)
+{
+ char *httpreq, *request, *reply;
+ JEntry *je;
+ Json *jv, *jstat, *jmsg;
+
+ request = makerequest(method, name1, arg);
+
+ je = nil;
+ if(usecache){
+ je = jcachelookup(request);
+ if(je->reply){
+ free(request);
+ return jincref(je->reply);
+ }
+ }
+
+ rpclog("%T %s", request);
+ httpreq = makehttprequest(host, path, request);
+ free(request);
+
+ if((reply = dojsonhttp(proto, host, httpreq, -1, 0)) == nil){
+ free(httpreq);
+ return nil;
+ }
+ free(httpreq);
+
+ jv = parsejson(reply);
+ free(reply);
+ if(jv == nil){
+ rpclog("%s: error parsing JSON reply: %r", method);
+ return nil;
+ }
+
+ if(jstrcmp((jstat = jlookup(jv, "stat")), "ok") == 0){
+ if(je)
+ je->reply = jincref(jv);
+ return jv;
+ }
+
+ if(jstrcmp(jstat, "fail") == 0){
+ jmsg = jlookup(jv, "message");
+ if(jmsg){
+ // If there are no images, that's not an error!
+ // (But SmugMug says it is.)
+ if(strcmp(method, "smugmug.images.get") == 0 &&
+ jstrcmp(jmsg, "empty set - no images found") == 0){
+ jclose(jv);
+ jv = parsejson("{\"stat\":\"ok\", \"Images\":[]}");
+ if(jv == nil)
+ sysfatal("parsejson: %r");
+ je->reply = jincref(jv);
+ return jv;
+ }
+ if(printerrors)
+ fprint(2, "%s: %J\n", method, jv);
+ rpclog("%s: %J", method, jmsg);
+ werrstr("%J", jmsg);
+ jclose(jv);
+ return nil;
+ }
+ rpclog("%s: json status: %J", method, jstat);
+ jclose(jv);
+ return nil;
+ }
+
+ rpclog("%s: json stat=%J", method, jstat);
+ jclose(jv);
+ return nil;
+}
+
+Json*
+ncsmug(char *method, char *name1, ...)
+{
+ Json *jv;
+ va_list arg;
+
+ va_start(arg, name1);
+ // TODO: Could use https only for login.
+ jv = jsonrpc(&https, HOST, PATH, method, name1, arg, 0);
+ va_end(arg);
+ rpclog("reply: %J", jv);
+ return jv;
+}
+
+Json*
+smug(char *method, char *name1, ...)
+{
+ Json *jv;
+ va_list arg;
+
+ va_start(arg, name1);
+ jv = jsonrpc(&http, HOST, PATH, method, name1, arg, 1);
+ va_end(arg);
+ return jv;
+}
+
+Json*
+jsonupload(Protocol *proto, char *host, char *req, int rfd, vlong rlength)
+{
+ Json *jv, *jstat, *jmsg;
+ char *reply;
+
+ if((reply = dojsonhttp(proto, host, req, rfd, rlength)) == nil)
+ return nil;
+
+ jv = parsejson(reply);
+ free(reply);
+ if(jv == nil){
+ fprint(2, "upload: error parsing JSON reply\n");
+ return nil;
+ }
+
+ if(jstrcmp((jstat = jlookup(jv, "stat")), "ok") == 0)
+ return jv;
+
+ if(jstrcmp(jstat, "fail") == 0){
+ jmsg = jlookup(jv, "message");
+ if(jmsg){
+ fprint(2, "upload: %J\n", jmsg);
+ werrstr("%J", jmsg);
+ jclose(jv);
+ return nil;
+ }
+ fprint(2, "upload: json status: %J\n", jstat);
+ jclose(jv);
+ return nil;
+ }
+
+ fprint(2, "upload: %J\n", jv);
+ jclose(jv);
+ return nil;
+}
+
diff --git a/src/cmd/smugfs/log.c b/src/cmd/smugfs/log.c
new file mode 100644
index 00000000..5603211e
--- /dev/null
+++ b/src/cmd/smugfs/log.c
@@ -0,0 +1,120 @@
+#include "a.h"
+
+void
+lbkick(Logbuf *lb)
+{
+ char *s;
+ int n;
+ Req *r;
+
+ while(lb->wait && lb->rp != lb->wp){
+ r = lb->wait;
+ lb->wait = r->aux;
+ if(lb->wait == nil)
+ lb->waitlast = &lb->wait;
+ r->aux = nil;
+ if(r->ifcall.count < 5){
+ respond(r, "log read request count too short");
+ continue;
+ }
+ s = lb->msg[lb->rp];
+ lb->msg[lb->rp] = nil;
+ if(++lb->rp == nelem(lb->msg))
+ lb->rp = 0;
+ n = r->ifcall.count;
+ if(n < strlen(s)+1+1){
+ memmove(r->ofcall.data, s, n-5);
+ n -= 5;
+ r->ofcall.data[n] = '\0';
+ /* look for first byte of UTF-8 sequence by skipping continuation bytes */
+ while(n>0 && (r->ofcall.data[--n]&0xC0)==0x80)
+ ;
+ strcpy(r->ofcall.data+n, "...\n");
+ }else{
+ strcpy(r->ofcall.data, s);
+ strcat(r->ofcall.data, "\n");
+ }
+ r->ofcall.count = strlen(r->ofcall.data);
+ free(s);
+ respond(r, nil);
+ }
+}
+
+void
+lbread(Logbuf *lb, Req *r)
+{
+ if(lb->waitlast == nil)
+ lb->waitlast = &lb->wait;
+ *lb->waitlast = r;
+ lb->waitlast = (Req**)(void*)&r->aux;
+ r->aux = nil;
+ lbkick(lb);
+}
+
+void
+lbflush(Logbuf *lb, Req *r)
+{
+ Req **l;
+
+ for(l=&lb->wait; *l; l=(Req**)(void*)&(*l)->aux){
+ if(*l == r){
+ *l = r->aux;
+ r->aux = nil;
+ if(*l == nil)
+ lb->waitlast = l;
+ respond(r, "interrupted");
+ break;
+ }
+ }
+}
+
+void
+lbappend(Logbuf *lb, char *fmt, ...)
+{
+ va_list arg;
+
+ va_start(arg, fmt);
+ lbvappend(lb, fmt, arg);
+ va_end(arg);
+}
+
+void
+lbvappend(Logbuf *lb, char *fmt, va_list arg)
+{
+ char *s;
+
+ s = vsmprint(fmt, arg);
+ if(s == nil)
+ sysfatal("out of memory");
+ if(lb->msg[lb->wp])
+ free(lb->msg[lb->wp]);
+ lb->msg[lb->wp] = s;
+ if(++lb->wp == nelem(lb->msg))
+ lb->wp = 0;
+ lbkick(lb);
+}
+
+Logbuf rpclogbuf;
+
+void
+rpclogread(Req *r)
+{
+ lbread(&rpclogbuf, r);
+}
+
+void
+rpclogflush(Req *r)
+{
+ lbflush(&rpclogbuf, r);
+}
+
+void
+rpclog(char *fmt, ...)
+{
+ va_list arg;
+
+ va_start(arg, fmt);
+ lbvappend(&rpclogbuf, fmt, arg);
+ va_end(arg);
+}
+
diff --git a/src/cmd/smugfs/main.c b/src/cmd/smugfs/main.c
new file mode 100644
index 00000000..6be0ca42
--- /dev/null
+++ b/src/cmd/smugfs/main.c
@@ -0,0 +1,108 @@
+#include "a.h"
+
+char *keypattern = "";
+char *sessid;
+Json *userinfo;
+int printerrors;
+
+void
+usage(void)
+{
+ fprint(2, "usage: smugfs [-k keypattern] [-m mtpt] [-s srv]\n");
+ threadexitsall("usage");
+}
+
+void
+smuglogin(void)
+{
+ Json *v;
+ char *s;
+ UserPasswd *up;
+
+ printerrors = 1;
+ up = auth_getuserpasswd(auth_getkey,
+ "proto=pass role=client server=smugmug.com "
+ "user? !password? %s", keypattern);
+ if(up == nil)
+ sysfatal("cannot get username/password: %r");
+
+ v = ncsmug("smugmug.login.withPassword",
+ "EmailAddress", up->user,
+ "Password", up->passwd,
+ nil);
+ if(v == nil)
+ sysfatal("login failed: %r");
+
+ memset(up->user, 'X', strlen(up->user));
+ memset(up->passwd, 'X', strlen(up->passwd));
+ free(up);
+
+ sessid = jstring(jwalk(v, "Login/Session/id"));
+ if(sessid == nil)
+ sysfatal("no session id");
+ sessid = estrdup(sessid);
+ s = jstring(jwalk(v, "Login/User/NickName"));
+ if(s == nil)
+ sysfatal("no nick name");
+ if(nickindex(s) != 0)
+ sysfatal("bad nick name");
+ userinfo = jincref(jwalk(v, "Login"));
+ jclose(v);
+ printerrors = 0;
+}
+
+void
+threadmain(int argc, char **argv)
+{
+ char *mtpt, *name;
+
+ mtpt = nil;
+ name = nil;
+ ARGBEGIN{
+ case 'D':
+ chatty9p++;
+ break;
+ case 'F':
+ chattyfuse++;
+ break;
+ case 'H':
+ chattyhttp++;
+ break;
+ case 'm':
+ mtpt = EARGF(usage());
+ break;
+ case 's':
+ name = EARGF(usage());
+ break;
+ case 'k':
+ keypattern = EARGF(usage());
+ break;
+ default:
+ usage();
+ }ARGEND
+
+ if(argc != 0)
+ usage();
+
+ if(name == nil && mtpt == nil)
+ mtpt = "/n/smug";
+
+ /*
+ * Check twice -- if there is an exited smugfs instance
+ * mounted there, the first access will fail but unmount it.
+ */
+ if(mtpt && access(mtpt, AEXIST) < 0 && access(mtpt, AEXIST) < 0)
+ sysfatal("mountpoint %s does not exist", mtpt);
+
+ fmtinstall('H', encodefmt);
+ fmtinstall('[', encodefmt); // base-64
+ fmtinstall('J', jsonfmt);
+ fmtinstall('M', dirmodefmt);
+ fmtinstall('T', timefmt);
+ fmtinstall('U', urlencodefmt);
+
+ xinit();
+ smuglogin();
+ threadpostmountsrv(&xsrv, name, mtpt, 0);
+ threadexits(nil);
+}
diff --git a/src/cmd/smugfs/mkfile b/src/cmd/smugfs/mkfile
new file mode 100644
index 00000000..c6b192f7
--- /dev/null
+++ b/src/cmd/smugfs/mkfile
@@ -0,0 +1,21 @@
+<$PLAN9/src/mkhdr
+
+TARG=smugfs
+
+HFILES=a.h
+
+OFILES=\
+ cache.$O\
+ download.$O\
+ fs.$O\
+ http.$O\
+ json.$O\
+ jsonrpc.$O\
+ log.$O\
+ main.$O\
+ openssl.$O\
+ tcp.$O\
+ util.$O\
+
+<$PLAN9/src/mkone
+
diff --git a/src/cmd/smugfs/openssl.c b/src/cmd/smugfs/openssl.c
new file mode 100644
index 00000000..baccd3ac
--- /dev/null
+++ b/src/cmd/smugfs/openssl.c
@@ -0,0 +1,98 @@
+#include <u.h>
+#include <openssl/bio.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include "a.h"
+
+AUTOLIB(ssl)
+
+static void
+httpsinit(void)
+{
+ ERR_load_crypto_strings();
+ ERR_load_SSL_strings();
+ SSL_load_error_strings();
+ SSL_library_init();
+}
+
+struct Pfd
+{
+ BIO *sbio;
+};
+
+static Pfd*
+opensslconnect(char *host)
+{
+ Pfd *pfd;
+ BIO *sbio;
+ SSL_CTX *ctx;
+ SSL *ssl;
+ static int didinit;
+ char buf[1024];
+
+ if(!didinit){
+ httpsinit();
+ didinit = 1;
+ }
+
+ ctx = SSL_CTX_new(SSLv23_client_method());
+ sbio = BIO_new_ssl_connect(ctx);
+ BIO_get_ssl(sbio, &ssl);
+ SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
+
+ snprint(buf, sizeof buf, "%s:https", host);
+ BIO_set_conn_hostname(sbio, buf);
+
+ if(BIO_do_connect(sbio) <= 0 || BIO_do_handshake(sbio) <= 0){
+ ERR_error_string_n(ERR_get_error(), buf, sizeof buf);
+ BIO_free_all(sbio);
+ werrstr("openssl: %s", buf);
+ return nil;
+ }
+
+ pfd = emalloc(sizeof *pfd);
+ pfd->sbio = sbio;
+ return pfd;
+}
+
+static void
+opensslclose(Pfd *pfd)
+{
+ if(pfd == nil)
+ return;
+ BIO_free_all(pfd->sbio);
+ free(pfd);
+}
+
+static int
+opensslwrite(Pfd *pfd, void *v, int n)
+{
+ int m, total;
+ char *p;
+
+ p = v;
+ total = 0;
+ while(total < n){
+ if((m = BIO_write(pfd->sbio, p+total, n-total)) <= 0){
+ if(total == 0)
+ return m;
+ return total;
+ }
+ total += m;
+ }
+ return total;
+}
+
+static int
+opensslread(Pfd *pfd, void *v, int n)
+{
+ return BIO_read(pfd->sbio, v, n);
+}
+
+Protocol https =
+{
+ opensslconnect,
+ opensslread,
+ opensslwrite,
+ opensslclose
+};
diff --git a/src/cmd/smugfs/tcp.c b/src/cmd/smugfs/tcp.c
new file mode 100644
index 00000000..a203ece9
--- /dev/null
+++ b/src/cmd/smugfs/tcp.c
@@ -0,0 +1,50 @@
+#include "a.h"
+
+struct Pfd
+{
+ int fd;
+};
+
+static Pfd*
+httpconnect(char *host)
+{
+ char buf[1024];
+ Pfd *pfd;
+ int fd;
+
+ snprint(buf, sizeof buf, "tcp!%s!http", host);
+ if((fd = dial(buf, nil, nil, nil)) < 0)
+ return nil;
+ pfd = emalloc(sizeof *pfd);
+ pfd->fd = fd;
+ return pfd;
+}
+
+static void
+httpclose(Pfd *pfd)
+{
+ if(pfd == nil)
+ return;
+ close(pfd->fd);
+ free(pfd);
+}
+
+static int
+httpwrite(Pfd *pfd, void *v, int n)
+{
+ return writen(pfd->fd, v, n);
+}
+
+static int
+httpread(Pfd *pfd, void *v, int n)
+{
+ return read(pfd->fd, v, n);
+}
+
+Protocol http = {
+ httpconnect,
+ httpread,
+ httpwrite,
+ httpclose,
+};
+
diff --git a/src/cmd/smugfs/util.c b/src/cmd/smugfs/util.c
new file mode 100644
index 00000000..b4a649d8
--- /dev/null
+++ b/src/cmd/smugfs/util.c
@@ -0,0 +1,81 @@
+#include "a.h"
+
+void*
+emalloc(int n)
+{
+ void *v;
+
+ v = mallocz(n, 1);
+ if(v == nil)
+ sysfatal("out of memory");
+ return v;
+}
+
+void*
+erealloc(void *v, int n)
+{
+ v = realloc(v, n);
+ if(v == nil)
+ sysfatal("out of memory");
+ return v;
+}
+
+char*
+estrdup(char *s)
+{
+ s = strdup(s);
+ if(s == nil)
+ sysfatal("out of memory");
+ return s;
+}
+
+int
+timefmt(Fmt *f)
+{
+ Tm tm;
+ vlong ms;
+
+ ms = nsec()/1000000;
+
+ tm = *localtime(ms/1000);
+ fmtprint(f, "%02d:%02d:%02d.%03d",
+ tm.hour, tm.min, tm.sec,
+ (int)(ms%1000));
+ return 0;
+}
+
+int
+writen(int fd, void *buf, int n)
+{
+ long m, tot;
+
+ for(tot=0; tot<n; tot+=m){
+ m = n - tot;
+ if(m > 8192)
+ m = 8192;
+ if(write(fd, (uchar*)buf+tot, m) != m)
+ break;
+ }
+ return tot;
+}
+
+int
+urlencodefmt(Fmt *fmt)
+{
+ int x;
+ char *s;
+
+ s = va_arg(fmt->args, char*);
+ for(; *s; s++){
+ x = (uchar)*s;
+ if(x == ' ')
+ fmtrune(fmt, '+');
+ else if(('a' <= x && x <= 'z') || ('A' <= x && x <= 'Z') || ('0' <= x && x <= '9')
+ || strchr("$-_.+!*'()", x)){
+ fmtrune(fmt, x);
+ }else
+ fmtprint(fmt, "%%%02ux", x);
+ }
+ return 0;
+}
+