tsmugfs(4): new program - plan9port - [fork] Plan 9 from user space
 (HTM) git clone git://src.adamsgaard.dk/plan9port
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 18824b586835525594cde126fbc90b8281d5af8b
 (DIR) parent 3d36f4437348227c5bad62587dc12b5fd4a3e95e
 (HTM) Author: Russ Cox <rsc@swtch.com>
       Date:   Sun,  3 Aug 2008 07:42:27 -0700
       
       smugfs(4): new program
       
       Diffstat:
         A man/man4/smugfs.4                   |     278 ++++++++++++++++++++++++++++++
         A src/cmd/smugfs/COPYRIGHT            |      17 +++++++++++++++++
         A src/cmd/smugfs/NOTES                |      22 ++++++++++++++++++++++
         A src/cmd/smugfs/a.h                  |     190 ++++++++++++++++++++++++++++++
         A src/cmd/smugfs/cache.c              |     149 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/download.c           |     105 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/fs.c                 |    1853 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/http.c               |     237 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/icache.c             |     171 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/json.c               |     555 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/jsonrpc.c            |     244 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/log.c                |     120 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/main.c               |     108 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/mkfile               |      21 +++++++++++++++++++++
         A src/cmd/smugfs/openssl.c            |      98 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/tcp.c                |      50 +++++++++++++++++++++++++++++++
         A src/cmd/smugfs/util.c               |      81 ++++++++++++++++++++++++++++++
       
       17 files changed, 4299 insertions(+), 0 deletions(-)
       ---
 (DIR) diff --git a/man/man4/smugfs.4 b/man/man4/smugfs.4
       t@@ -0,0 +1,278 @@
       +.TH SMUGFS 4
       +.SH NAME
       +smugfs \- file system access to SmugMug photo sharing
       +.SH SYNOPSIS
       +.B smugfs
       +[
       +.B -DFH
       +]
       +[
       +.B -k
       +.I keypattern
       +]
       +[
       +.B -m
       +.I mtpt
       +]
       +[
       +.B -s
       +.I srvname
       +]
       +.SH DESCRIPTION
       +.I Smugfs
       +is a user-level file system that provides access to images
       +stored on the SmugMug photo sharing service.
       +It logs in after
       +obtaining a password from 
       +.IR factotum (4)
       +using
       +.B server=smugmug.com
       +and
       +.I keypattern
       +(if any)
       +as key criteria
       +(see
       +.IR auth (3)).
       +Then 
       +.I smugfs
       +serves a virtual directory tree mounted at
       +.I mtpt
       +(default
       +.BR /n/smug )
       +and posted at 
       +.I srvname ,
       +if the 
       +.B -s
       +option is given.
       +.PP
       +The directory tree is arranged in five levels:
       +root, user, category, album, and image.
       +For example,
       +.B /n/smug/cmac/
       +is a user directory,
       +.B /n/smug/cmac/People/
       +is a category directory,
       +.B /n/smug/cmac/People/Friends/
       +is an album directory,
       +and
       +.B /n/smug/cmac/albums/Friends/2631/
       +is an image directory.
       +.PP
       +SmugMug allows fine-grained classification
       +via subcategories, but subcategories are not yet implemented.
       +.ig
       +  Subcategories are inserted as
       +an additional directory level between category 
       +and album.
       +[Subcategories are not yet implemented.]
       +..
       +.PP
       +All directories contain a special control file named
       +.BR ctl ;
       +text commands written to 
       +.B ctl
       +change 
       +.IR smugfs 's
       +behavior or implement functionality
       +that does not fit nicely into the file system
       +interface.
       +.PP
       +.I Smugfs
       +caches information about users, categories, albums,
       +and images.  If changes are made outside of
       +.I smugfs
       +(for example, using a web browser),
       +the cache may need to be discarded.
       +Writing the string
       +.B sync
       +to a directory's
       +.B ctl
       +file causes
       +.I smugfs
       +to discard all cached information used to
       +present that directory and its children.
       +Thus, writing
       +.B sync
       +to the root
       +.B ctl
       +file discards all of
       +.I smugfs 's
       +cached information.
       +.SS "Root directory"
       +The root directory contains directories
       +named after users.
       +By default, it contains only a directory for
       +the logged-in user, but other directories will
       +be created as needed to satisfy directory lookups.
       +.PP
       +In addition to user directories, the root directory
       +contains three special files:
       +.BR ctl ,
       +.BR rpclog ,
       +and
       +.BR uploads .
       +Reading
       +.B rpclog
       +returns a list of recent RPCs issued to the SmugMug API server.
       +Reads at the end of the file block until a new RPC is issued.
       +The
       +.B uploads
       +file lists the file upload queue (q.v.).
       +.SS "User directories"
       +User directories contain category directories
       +named after the categories.
       +SmugMug pre-defines a variety of categories,
       +so it is common to have many categories that
       +do not contain albums.
       +.PP
       +In a user directory, creating a new directory
       +creates a new category on SmugMug.
       +Similarly, renaming or removing a category
       +directory renames or removes the category on SmugMug.
       +Categories cannot be removed if they contain albums.
       +.PP
       +User directories also contain a directory
       +named
       +.B albums
       +that itself contains all of that user's albums.
       +.SS "Category directories"
       +Each category directory contains album directories
       +named using the album's title.
       +.PP
       +In a category directory, creating a new directory
       +creates a new album on SmugMug.
       +Similarly, renaming or removing an album directory
       +renames or removes the album on SmugMug.
       +Albums cannot be removed if they contain images.
       +.ig
       +.PP
       +Category directories might also contain subcategory directories.
       +Like albums, subcategories can be renamed and removed (when empty).
       +Unlike albums, subcategories cannot be created via ordinary
       +file system operations.
       +Instead, write the command
       +.B subcategory
       +.I name
       +to the category's
       +.B ctl
       +file.
       +.PP
       +Subcategories are identical to categories
       +except that they cannot themselves contain subcategories.
       +..
       +.SS "Album directories"
       +Each album directory contains image directories
       +named using the image's decimal SmugMug ID.
       +Image directories cannot be created or renamed,
       +but they can be removed.  Removing an image directory
       +removes the image from the album on SmugMug.
       +.PP
       +Album directories also contain three special files,
       +.BR ctl ,
       +.BR settings ,
       +and
       +.BR url .
       +.PP
       +The
       +.B settings
       +file contains the album settings in textual form,
       +one setting per line.
       +Each line represents a single setting and is formatted
       +as an alphabetic setting name followed by a single tab
       +followed by the value.
       +Many settings can be changed by writing new setting lines,
       +in the same format, to the
       +.B settings
       +file.
       +.PP
       +Copying a file into the album directory queues it for
       +uploading to SmugMug to be added to the album.
       +Files disappear from the album directory once they
       +have finished uploading, replaced by new image directories.
       +The 
       +.B uploads
       +file in the root directory lists all pending uploads,
       +which are stored temporarily
       +in 
       +.BR /var/tmp .
       +.SS "Image directories"
       +Each image directory contains an image file, named
       +with its original name, if available.
       +If the image belongs to another user, SmugMug does not
       +expose the original name, so the file is named
       +.RB \fInnnn\fP .jpg ,
       +where
       +.I nnnn
       +is the SmugMug image ID number.
       +The file content is the original image
       +or else the largest image available.
       +.PP
       +The directory contains a 
       +.B settings
       +file holding per-image settings, similar to the 
       +file in the album directory;
       +and a
       +.B url
       +file, containing URLs to the various sized images
       +on the SmugMug server.
       +.SH EXAMPLES
       +.LP
       +Mount
       +.I smugfs
       +on
       +.BR /n/smug ;
       +the current user must have write access to 
       +.B /n/smug
       +and
       +.BR /dev/fuse .
       +.IP
       +.EX
       +% smugfs
       +.EE
       +Watch API calls as they execute:
       +.IP
       +.EX
       +% cat /n/smug/rpclog &
       +.EE
       +Create a new album in the Vacation category
       +and fill it with photos:
       +.IP
       +.EX
       +% mkdir /n/smug/you/Vacation/Summer
       +% cp *.jpg /n/smug/you/Vacation/Summer
       +.EE
       +.LP
       +The photos are now uploading in the background.
       +Wait for the uploads to finish:
       +.IP
       +.EX
       +% while(test -s /n/smug/uploads) sleep 60
       +.EE
       +.LP
       +Make the album publicly viewable and share it.
       +.IP
       +.EX
       +% echo public 1 >/n/smug/you/Vacation/Summer/settings
       +% cat /n/smug/you/Vacation/Summer/url | mail friends
       +.EE
       +.SH SOURCE
       +.B \*9/src/cmd/smugfs
       +.SH SEE ALSO
       +SmugMug, 
       +.HR http://smugmug.com/
       +.SH BUGS
       +.PP
       +If multiple categories or albums have the same name,
       +only one will be accessible via the file system interface.
       +Renaming the accessible one via 
       +.IR mv (1)
       +will resolve the problem.
       +.PP
       +Boolean values appear as
       +.B true
       +and
       +.B false
       +in settings files but must be changed using
       +.B 1
       +and
       +.BR 0 .
 (DIR) diff --git a/src/cmd/smugfs/COPYRIGHT b/src/cmd/smugfs/COPYRIGHT
       t@@ -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.
       +
 (DIR) diff --git a/src/cmd/smugfs/NOTES b/src/cmd/smugfs/NOTES
       t@@ -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.
       +
 (DIR) diff --git a/src/cmd/smugfs/a.h b/src/cmd/smugfs/a.h
       t@@ -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;
       +
 (DIR) diff --git a/src/cmd/smugfs/cache.c b/src/cmd/smugfs/cache.c
       t@@ -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;
       +                }
       +        }
       +}
 (DIR) diff --git a/src/cmd/smugfs/download.c b/src/cmd/smugfs/download.c
       t@@ -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);
       +}
 (DIR) diff --git a/src/cmd/smugfs/fs.c b/src/cmd/smugfs/fs.c
       t@@ -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;
       +}
 (DIR) diff --git a/src/cmd/smugfs/http.c b/src/cmd/smugfs/http.c
       t@@ -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;
       +}
 (DIR) diff --git a/src/cmd/smugfs/icache.c b/src/cmd/smugfs/icache.c
       t@@ -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;
       +}
       +
 (DIR) diff --git a/src/cmd/smugfs/json.c b/src/cmd/smugfs/json.c
       t@@ -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);
       +}
 (DIR) diff --git a/src/cmd/smugfs/jsonrpc.c b/src/cmd/smugfs/jsonrpc.c
       t@@ -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;
       +}
       +
 (DIR) diff --git a/src/cmd/smugfs/log.c b/src/cmd/smugfs/log.c
       t@@ -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);
       +}
       +
 (DIR) diff --git a/src/cmd/smugfs/main.c b/src/cmd/smugfs/main.c
       t@@ -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);
       +}
 (DIR) diff --git a/src/cmd/smugfs/mkfile b/src/cmd/smugfs/mkfile
       t@@ -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
       +
 (DIR) diff --git a/src/cmd/smugfs/openssl.c b/src/cmd/smugfs/openssl.c
       t@@ -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
       +};
 (DIR) diff --git a/src/cmd/smugfs/tcp.c b/src/cmd/smugfs/tcp.c
       t@@ -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,
       +};
       +
 (DIR) diff --git a/src/cmd/smugfs/util.c b/src/cmd/smugfs/util.c
       t@@ -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;
       +}
       +