commit 18824b586835525594cde126fbc90b8281d5af8b from: Russ Cox date: Sun Aug 03 14:42:27 2008 UTC smugfs(4): new program commit - 3d36f4437348227c5bad62587dc12b5fd4a3e95e commit + 18824b586835525594cde126fbc90b8281d5af8b blob - /dev/null blob + f05ccf49018a8ea2512e88a6f65c23084f7279a5 (mode 644) --- /dev/null +++ man/man4/smugfs.4 @@ -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 . blob - /dev/null blob + 0741ccf54cc1872fdd9f19b5cee3134c3da620db (mode 644) --- /dev/null +++ src/cmd/smugfs/COPYRIGHT @@ -0,0 +1,17 @@ +The files in this directory are subject to the following license. + +The author of this software is Russ Cox. + + Copyright (c) 2008 Russ Cox + +Permission to use, copy, modify, and distribute this software for any +purpose without fee is hereby granted, provided that this entire notice +is included in all copies of any software which is or includes a copy +or modification of this software and in all copies of the supporting +documentation for such software. + +THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED +WARRANTY. IN PARTICULAR, THE AUTHOR MAKES NO REPRESENTATION OR WARRANTY +OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS SOFTWARE OR ITS +FITNESS FOR ANY PARTICULAR PURPOSE. + blob - /dev/null blob + b88999155c5cff46fe9513d328f9eca0978967ee (mode 644) --- /dev/null +++ src/cmd/smugfs/NOTES @@ -0,0 +1,22 @@ + +* Threading: + +Uploads run in parallel with main fs operation. +Otherwise, main fs operation is single-threaded. +Could multi-thread the rest but would have to lock the +cache properly first. + +Right now, only one upload at a time. +Could have more by kicking off multiple +uploader procs. + +* Implement subcategories. + +* Implement renames of categories. + +* Implement renames of albums. + +* Implement album settings file. + +* Implement image settings file. + blob - /dev/null blob + fa65002d7867e67a0b664a08d7c1478a12196832 (mode 644) --- /dev/null +++ src/cmd/smugfs/a.h @@ -0,0 +1,190 @@ +#include +#include +#include +#include +#include +#include +#include <9p.h> +#include + +#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; + blob - /dev/null blob + 2adf8b9b62d6583f2ba7db8abfb509810740d01d (mode 644) --- /dev/null +++ src/cmd/smugfs/cache.c @@ -0,0 +1,149 @@ +#include "a.h" + +struct Cache +{ + CEntry **hash; + int nhash; + CEntry *head; + CEntry *tail; + int nentry; + int maxentry; + int sizeofentry; + void (*cefree)(CEntry*); +}; + +static void +nop(CEntry *ce) +{ +} + +static uint +hash(const char *s) +{ + uint h; + uchar *p; + + h = 0; + for(p=(uchar*)s; *p; p++) + h = h*37 + *p; + return h; +} + +Cache* +newcache(int sizeofentry, int maxentry, void (*cefree)(CEntry*)) +{ + Cache *c; + int i; + + assert(sizeofentry >= sizeof(CEntry)); + c = emalloc(sizeof *c); + c->sizeofentry = sizeofentry; + c->maxentry = maxentry; + c->nentry = 0; + for(i=1; inhash = 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; inhash; 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; + } + } +} blob - /dev/null blob + e23d49e145a8cb77a08df7d0e9cf82370891149c (mode 644) --- /dev/null +++ src/cmd/smugfs/download.c @@ -0,0 +1,105 @@ +#include "a.h" + +typedef struct DEntry DEntry; +struct DEntry +{ + CEntry ce; + HTTPHeader hdr; + char *tmpfile; + int fd; +}; + +static void +dfree(CEntry *ce) +{ + DEntry *d; + + d = (DEntry*)ce; + if(d->tmpfile){ + remove(d->tmpfile); + free(d->tmpfile); + close(d->fd); + } +} + +static Cache *downloadcache; + +static char* +parseurl(char *url, char **path) +{ + char *host, *p; + int len; + + if(memcmp(url, "http://", 7) != 0) + return nil; + p = strchr(url+7, '/'); + if(p == nil) + p = url+strlen(url); + len = p - (url+7); + host = emalloc(len+1); + memmove(host, url+7, len); + host[len] = 0; + if(*p == 0) + p = "/"; + *path = p; + return host; +} + +int +download(char *url, HTTPHeader *hdr) +{ + DEntry *d; + char *host, *path; + char buf[] = "/var/tmp/smugfs.XXXXXX"; + char *req; + int fd; + Fmt fmt; + + if(downloadcache == nil) + downloadcache = newcache(sizeof(DEntry), 128, dfree); + + host = parseurl(url, &path); + if(host == nil) + return -1; + + d = (DEntry*)cachelookup(downloadcache, url, 1); + if(d->tmpfile){ + free(host); + *hdr = d->hdr; + return dup(d->fd, -1); + } + d->fd = -1; // paranoia + + if((fd = opentemp(buf, ORDWR|ORCLOSE)) < 0){ + free(host); + return -1; + } + + fmtstrinit(&fmt); + fmtprint(&fmt, "GET %s HTTP/1.0\r\n", path); + fmtprint(&fmt, "Host: %s\r\n", host); + fmtprint(&fmt, "User-Agent: " USER_AGENT "\r\n"); + fmtprint(&fmt, "\r\n"); + req = fmtstrflush(&fmt); + + fprint(2, "Get %s\n", url); + + if(httptofile(&http, host, req, hdr, fd) < 0){ + free(host); + free(req); + return -1; + } + free(host); + free(req); + d->tmpfile = estrdup(buf); + d->fd = dup(fd, -1); + d->hdr = *hdr; + return fd; +} + +void +downloadflush(char *substr) +{ + if(downloadcache) + cacheflush(downloadcache, substr); +} blob - /dev/null blob + 52b82051bc525d52959726240dbfbe05f2bcfcee (mode 644) --- /dev/null +++ src/cmd/smugfs/fs.c @@ -0,0 +1,1853 @@ +#include "a.h" + +enum +{ + Qroot = 0, // /smug/ + Qctl, // /smug/ctl + Qrpclog, // /smug/rpclog + Quploads, // /smug/uploads + Qnick, // /smug/nick/ + Qnickctl, // /smug/nick/ctl + Qalbums, // /smug/nick/albums/ + Qalbumsctl, // /smug/nick/albums/ctl + Qcategory, // /smug/nick/Category/ + Qcategoryctl, // /smug/nick/Category/ctl + Qalbum, // /smug/nick/Category/Album/ + Qalbumctl, // /smug/nick/Category/Album/ctl + Qalbumsettings, // /smug/nick/Category/Album/settings + Quploadfile, // /smug/nick/Category/Album/upload/file.jpg + Qimage, // /smug/nick/Category/Album/Image/ + Qimagectl, // /smug/nick/Category/Album/Image/ctl + Qimageexif, // /smug/nick/Category/Album/Image/exif + Qimagesettings, // /smug/nick/Category/Album/Image/settings + Qimageurl, // /smug/nick/Category/Album/Image/url + Qimagefile, // /smug/nick/Category/Album/Image/file.jpg +}; + +void +mylock(Lock *lk) +{ + lock(lk); + fprint(2, "locked from %p\n", getcallerpc(&lk)); +} + +void +myunlock(Lock *lk) +{ + unlock(lk); + fprint(2, "unlocked from %p\n", getcallerpc(&lk)); +} + +//#define lock mylock +//#define unlock myunlock + +typedef struct Upload Upload; + +typedef struct SmugFid SmugFid; +struct SmugFid +{ + int type; + int nickid; + vlong category; // -1 for "albums" + vlong album; + char *albumkey; + vlong image; + char *imagekey; + Upload *upload; + int upwriter; +}; + +#define QTYPE(p) ((p)&0xFF) +#define QARG(p) ((p)>>8) +#define QPATH(p, q) ((p)|((q)<<8)) + +char **nick; +int nnick; + +struct Upload +{ + Lock lk; + int fd; + char *name; + char *file; + vlong album; + vlong length; + char *albumkey; + int size; + int ready; + int nwriters; + int uploaded; + int ref; + int uploading; +}; + +Upload **up; +int nup; +QLock uploadlock; +Rendez uploadrendez; + +void uploader(void*); + +Upload* +newupload(SmugFid *sf, char *name) +{ + Upload *u; + int fd, i; + char tmp[] = "/var/tmp/smugfs.XXXXXX"; + + if((fd = opentemp(tmp, ORDWR)) < 0) + return nil; + qlock(&uploadlock); + for(i=0; ilk); + 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; ilk); + 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; ilk); + 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; ilk); + 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) + 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; ilen; 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; ilen; 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; ilen; 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; ilen; 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; ilk); + 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; iimage, 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; iimage, 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; itype, 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; +} blob - /dev/null blob + 9cf7f1d0729290ee40ac16f0d3e944dc165238a6 (mode 644) --- /dev/null +++ src/cmd/smugfs/http.c @@ -0,0 +1,237 @@ +#include "a.h" + +static char* +haveheader(char *buf, int n) +{ + int i; + + for(i=0; i 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; +} blob - /dev/null blob + 750f232c069e7a7ff386a4da1a02fdc7fc7e799a (mode 644) --- /dev/null +++ src/cmd/smugfs/icache.c @@ -0,0 +1,171 @@ +#include "a.h" + +// This code is almost certainly wrong. + +typedef struct Icache Icache; +struct Icache +{ + char *url; + HTTPHeader hdr; + char *tmpfile; + int fd; + Icache *next; + Icache *prev; + Icache *hash; +}; + +enum { + NHASH = 128, + MAXCACHE = 128, +}; +static struct { + Icache *hash[NHASH]; + Icache *head; + Icache *tail; + int n; +} icache; + +static Icache* +icachefind(char *url) +{ + int h; + Icache *ic; + + h = hash(url) % NHASH; + for(ic=icache.hash[h]; ic; ic=ic->hash){ + if(strcmp(ic->url, url) == 0){ + /* move to front */ + if(ic->prev) { + ic->prev->next = ic->next; + if(ic->next) + ic->next->prev = ic->prev; + else + icache.tail = ic->prev; + ic->prev = nil; + ic->next = icache.head; + icache.head->prev = ic; + icache.head = ic; + } + return ic; + } + } + return nil; +} + +static Icache* +icacheinsert(char *url, HTTPHeader *hdr, char *file, int fd) +{ + int h; + Icache *ic, **l; + + if(icache.n == MAXCACHE){ + ic = icache.tail; + icache.tail = ic->prev; + if(ic->prev) + ic->prev->next = nil; + else + icache.head = ic->prev; + h = hash(ic->url) % NHASH; + for(l=&icache.hash[h]; *l; l=&(*l)->hash){ + if(*l == ic){ + *l = ic->hash; + goto removed; + } + } + sysfatal("cannot find ic in cache"); + removed: + free(ic->url); + close(ic->fd); + remove(ic->file); + free(ic->file); + }else{ + ic = emalloc(sizeof *ic); + icache.n++; + } + + ic->url = estrdup(url); + ic->fd = dup(fd, -1); + ic->file = estrdup(file); + ic->hdr = *hdr; + h = hash(url) % NHASH; + ic->hash = icache.hash[h]; + icache.hash[h] = ic; + ic->prev = nil; + ic->next = icache.head; + if(ic->next) + ic->next->prev = ic; + else + icache.tail = ic; + return ic; +} + +void +icacheflush(char *substr) +{ + Icache **l, *ic; + + for(l=&icache.head; (ic=*l); ) { + if(substr == nil || strstr(ic->url, substr)) { + icache.n--; + *l = ic->next; + free(ic->url); + close(ic->fd); + remove(ic->file); + free(ic->file); + free(ic); + }else + l = &ic->next; + } + + if(icache.head) { + icache.head->prev = nil; + for(ic=icache.head; ic; ic=ic->next){ + if(ic->next) + ic->next->prev = ic; + else + icache.tail = ic; + } + }else + icache.tail = nil; +} + +int +urlfetch(char *url, HTTPHeader hdr) +{ + Icache *ic; + char buf[50], *host, *path, *p; + int fd, len; + + ic = icachefind(url); + if(ic != nil){ + *hdr = ic->hdr; + return dup(ic->fd, -1); + } + + if(memcmp(url, "http://", 7) != 0){ + werrstr("non-http url"); + return -1; + } + p = strchr(url+7, '/'); + if(p == nil) + p = url+strlen(url); + len = p - (url+7); + host = emalloc(len+1); + memmove(host, url+7, len); + host[len] = 0; + if(*p == 0) + p = "/"; + + strcpy(buf, "/var/tmp/smugfs.XXXXXX"); + fd = opentemp(buf, ORDWR|ORCLOSE); + if(fd < 0) + return -1; + if(httptofile(http, host, req, &hdr, fd) < 0){ + free(host); + return -1; + } + free(host); + icacheinsert(url, &hdr, buf, fd); + return fd; +} + blob - /dev/null blob + d6472b4db0a71cb7d11413ecea566cf11a82bc63 (mode 644) --- /dev/null +++ src/cmd/smugfs/json.c @@ -0,0 +1,555 @@ +#include "a.h" + +static Json *parsevalue(char**); + +static char* +wskip(char *p) +{ + while(*p == ' ' || *p == '\t' || *p == '\n' || *p == '\v') + p++; + return p; +} + +static int +ishex(int c) +{ + return '0' <= c && c <= '9' || + 'a' <= c && c <= 'f' || + 'A' <= c && c <= 'F'; +} + +static Json* +newjval(int type) +{ + Json *v; + + v = emalloc(sizeof *v); + v->ref = 1; + v->type = type; + return v; +} + +static Json* +badjval(char **pp, char *fmt, ...) +{ + char buf[ERRMAX]; + va_list arg; + + if(fmt){ + va_start(arg, fmt); + vsnprint(buf, sizeof buf, fmt, arg); + va_end(arg); + errstr(buf, sizeof buf); + } + *pp = nil; + return nil; +} + +static char* +_parsestring(char **pp, int *len) +{ + char *p, *q, *w, *s, *r; + char buf[5]; + Rune rune; + + p = wskip(*pp); + if(*p != '"'){ + badjval(pp, "missing opening quote for string"); + return nil; + } + for(q=p+1; *q && *q != '\"'; q++){ + if(*q == '\\' && *(q+1) != 0) + q++; + if((*q & 0xFF) < 0x20){ // no control chars + badjval(pp, "control char in string"); + return nil; + } + } + if(*q == 0){ + badjval(pp, "no closing quote in string"); + return nil; + } + s = emalloc(q - p); + w = s; + for(r=p+1; r '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; ilen; 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; ilen; 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; ilen; i++) + jclose(v->value[i]); + free(v->value); + break; + case Jobject: + for(i=0; ilen; 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; ilen; 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); +} blob - /dev/null blob + 92490e775a5e696bf24ceaa7969f76e603872ac0 (mode 644) --- /dev/null +++ src/cmd/smugfs/jsonrpc.c @@ -0,0 +1,244 @@ +#include "a.h" + +// JSON request/reply cache. + +int chattyhttp; + +typedef struct JEntry JEntry; +struct JEntry +{ + CEntry ce; + Json *reply; +}; + +static Cache *jsoncache; + +static void +jfree(CEntry *ce) +{ + JEntry *j; + + j = (JEntry*)ce; + jclose(j->reply); +} + +static JEntry* +jcachelookup(char *request) +{ + if(jsoncache == nil) + jsoncache = newcache(sizeof(JEntry), 1000, jfree); + return (JEntry*)cachelookup(jsoncache, request, 1); +} + +void +jcacheflush(char *substr) +{ + if(jsoncache == nil) + return; + cacheflush(jsoncache, substr); +} + + +// JSON RPC over HTTP + +static char* +makehttprequest(char *host, char *path, char *postdata) +{ + Fmt fmt; + + fmtstrinit(&fmt); + fmtprint(&fmt, "POST %s HTTP/1.0\r\n", path); + fmtprint(&fmt, "Host: %s\r\n", host); + fmtprint(&fmt, "User-Agent: " USER_AGENT "\r\n"); + fmtprint(&fmt, "Content-Type: application/x-www-form-urlencoded\r\n"); + fmtprint(&fmt, "Content-Length: %d\r\n", strlen(postdata)); + fmtprint(&fmt, "\r\n"); + fmtprint(&fmt, "%s", postdata); + return fmtstrflush(&fmt); +} + +static char* +makerequest(char *method, char *name1, va_list arg) +{ + char *p, *key, *val; + Fmt fmt; + + fmtstrinit(&fmt); + fmtprint(&fmt, "&"); + p = name1; + while(p != nil){ + key = p; + val = va_arg(arg, char*); + if(val == nil) + sysfatal("jsonrpc: nil value"); + fmtprint(&fmt, "%U=%U&", key, val); + p = va_arg(arg, char*); + } + // TODO: These are SmugMug-specific, probably. + fmtprint(&fmt, "method=%s&", method); + if(sessid) + fmtprint(&fmt, "SessionID=%s&", sessid); + fmtprint(&fmt, "APIKey=%s", APIKEY); + return fmtstrflush(&fmt); +} + +static char* +dojsonhttp(Protocol *proto, char *host, char *request, int rfd, vlong rlength) +{ + char *data; + HTTPHeader hdr; + + data = httpreq(proto, host, request, &hdr, rfd, rlength); + if(data == nil){ + fprint(2, "httpreq: %r\n"); + return nil; + } + if(strcmp(hdr.contenttype, "application/json") != 0 && + (strcmp(hdr.contenttype, "text/html; charset=utf-8") != 0 || data[0] != '{')){ // upload.smugmug.com, sigh + werrstr("bad content type: %s", hdr.contenttype); + fprint(2, "Content-Type: %s\n", hdr.contenttype); + write(2, data, hdr.contentlength); + return nil; + } + if(hdr.contentlength == 0){ + werrstr("no content"); + return nil; + } + return data; +} + +Json* +jsonrpc(Protocol *proto, char *host, char *path, char *method, char *name1, va_list arg, int usecache) +{ + char *httpreq, *request, *reply; + JEntry *je; + Json *jv, *jstat, *jmsg; + + request = makerequest(method, name1, arg); + + je = nil; + if(usecache){ + je = jcachelookup(request); + if(je->reply){ + free(request); + return jincref(je->reply); + } + } + + rpclog("%T %s", request); + httpreq = makehttprequest(host, path, request); + free(request); + + if((reply = dojsonhttp(proto, host, httpreq, -1, 0)) == nil){ + free(httpreq); + return nil; + } + free(httpreq); + + jv = parsejson(reply); + free(reply); + if(jv == nil){ + rpclog("%s: error parsing JSON reply: %r", method); + return nil; + } + + if(jstrcmp((jstat = jlookup(jv, "stat")), "ok") == 0){ + if(je) + je->reply = jincref(jv); + return jv; + } + + if(jstrcmp(jstat, "fail") == 0){ + jmsg = jlookup(jv, "message"); + if(jmsg){ + // If there are no images, that's not an error! + // (But SmugMug says it is.) + if(strcmp(method, "smugmug.images.get") == 0 && + jstrcmp(jmsg, "empty set - no images found") == 0){ + jclose(jv); + jv = parsejson("{\"stat\":\"ok\", \"Images\":[]}"); + if(jv == nil) + sysfatal("parsejson: %r"); + je->reply = jincref(jv); + return jv; + } + if(printerrors) + fprint(2, "%s: %J\n", method, jv); + rpclog("%s: %J", method, jmsg); + werrstr("%J", jmsg); + jclose(jv); + return nil; + } + rpclog("%s: json status: %J", method, jstat); + jclose(jv); + return nil; + } + + rpclog("%s: json stat=%J", method, jstat); + jclose(jv); + return nil; +} + +Json* +ncsmug(char *method, char *name1, ...) +{ + Json *jv; + va_list arg; + + va_start(arg, name1); + // TODO: Could use https only for login. + jv = jsonrpc(&https, HOST, PATH, method, name1, arg, 0); + va_end(arg); + rpclog("reply: %J", jv); + return jv; +} + +Json* +smug(char *method, char *name1, ...) +{ + Json *jv; + va_list arg; + + va_start(arg, name1); + jv = jsonrpc(&http, HOST, PATH, method, name1, arg, 1); + va_end(arg); + return jv; +} + +Json* +jsonupload(Protocol *proto, char *host, char *req, int rfd, vlong rlength) +{ + Json *jv, *jstat, *jmsg; + char *reply; + + if((reply = dojsonhttp(proto, host, req, rfd, rlength)) == nil) + return nil; + + jv = parsejson(reply); + free(reply); + if(jv == nil){ + fprint(2, "upload: error parsing JSON reply\n"); + return nil; + } + + if(jstrcmp((jstat = jlookup(jv, "stat")), "ok") == 0) + return jv; + + if(jstrcmp(jstat, "fail") == 0){ + jmsg = jlookup(jv, "message"); + if(jmsg){ + fprint(2, "upload: %J\n", jmsg); + werrstr("%J", jmsg); + jclose(jv); + return nil; + } + fprint(2, "upload: json status: %J\n", jstat); + jclose(jv); + return nil; + } + + fprint(2, "upload: %J\n", jv); + jclose(jv); + return nil; +} + blob - /dev/null blob + 5603211e317f4f3bbec3319233d6316b81a7d5a8 (mode 644) --- /dev/null +++ src/cmd/smugfs/log.c @@ -0,0 +1,120 @@ +#include "a.h" + +void +lbkick(Logbuf *lb) +{ + char *s; + int n; + Req *r; + + while(lb->wait && lb->rp != lb->wp){ + r = lb->wait; + lb->wait = r->aux; + if(lb->wait == nil) + lb->waitlast = &lb->wait; + r->aux = nil; + if(r->ifcall.count < 5){ + respond(r, "log read request count too short"); + continue; + } + s = lb->msg[lb->rp]; + lb->msg[lb->rp] = nil; + if(++lb->rp == nelem(lb->msg)) + lb->rp = 0; + n = r->ifcall.count; + if(n < strlen(s)+1+1){ + memmove(r->ofcall.data, s, n-5); + n -= 5; + r->ofcall.data[n] = '\0'; + /* look for first byte of UTF-8 sequence by skipping continuation bytes */ + while(n>0 && (r->ofcall.data[--n]&0xC0)==0x80) + ; + strcpy(r->ofcall.data+n, "...\n"); + }else{ + strcpy(r->ofcall.data, s); + strcat(r->ofcall.data, "\n"); + } + r->ofcall.count = strlen(r->ofcall.data); + free(s); + respond(r, nil); + } +} + +void +lbread(Logbuf *lb, Req *r) +{ + if(lb->waitlast == nil) + lb->waitlast = &lb->wait; + *lb->waitlast = r; + lb->waitlast = (Req**)(void*)&r->aux; + r->aux = nil; + lbkick(lb); +} + +void +lbflush(Logbuf *lb, Req *r) +{ + Req **l; + + for(l=&lb->wait; *l; l=(Req**)(void*)&(*l)->aux){ + if(*l == r){ + *l = r->aux; + r->aux = nil; + if(*l == nil) + lb->waitlast = l; + respond(r, "interrupted"); + break; + } + } +} + +void +lbappend(Logbuf *lb, char *fmt, ...) +{ + va_list arg; + + va_start(arg, fmt); + lbvappend(lb, fmt, arg); + va_end(arg); +} + +void +lbvappend(Logbuf *lb, char *fmt, va_list arg) +{ + char *s; + + s = vsmprint(fmt, arg); + if(s == nil) + sysfatal("out of memory"); + if(lb->msg[lb->wp]) + free(lb->msg[lb->wp]); + lb->msg[lb->wp] = s; + if(++lb->wp == nelem(lb->msg)) + lb->wp = 0; + lbkick(lb); +} + +Logbuf rpclogbuf; + +void +rpclogread(Req *r) +{ + lbread(&rpclogbuf, r); +} + +void +rpclogflush(Req *r) +{ + lbflush(&rpclogbuf, r); +} + +void +rpclog(char *fmt, ...) +{ + va_list arg; + + va_start(arg, fmt); + lbvappend(&rpclogbuf, fmt, arg); + va_end(arg); +} + blob - /dev/null blob + 6be0ca42d339a0377bdca16452a2eb5926f37542 (mode 644) --- /dev/null +++ src/cmd/smugfs/main.c @@ -0,0 +1,108 @@ +#include "a.h" + +char *keypattern = ""; +char *sessid; +Json *userinfo; +int printerrors; + +void +usage(void) +{ + fprint(2, "usage: smugfs [-k keypattern] [-m mtpt] [-s srv]\n"); + threadexitsall("usage"); +} + +void +smuglogin(void) +{ + Json *v; + char *s; + UserPasswd *up; + + printerrors = 1; + up = auth_getuserpasswd(auth_getkey, + "proto=pass role=client server=smugmug.com " + "user? !password? %s", keypattern); + if(up == nil) + sysfatal("cannot get username/password: %r"); + + v = ncsmug("smugmug.login.withPassword", + "EmailAddress", up->user, + "Password", up->passwd, + nil); + if(v == nil) + sysfatal("login failed: %r"); + + memset(up->user, 'X', strlen(up->user)); + memset(up->passwd, 'X', strlen(up->passwd)); + free(up); + + sessid = jstring(jwalk(v, "Login/Session/id")); + if(sessid == nil) + sysfatal("no session id"); + sessid = estrdup(sessid); + s = jstring(jwalk(v, "Login/User/NickName")); + if(s == nil) + sysfatal("no nick name"); + if(nickindex(s) != 0) + sysfatal("bad nick name"); + userinfo = jincref(jwalk(v, "Login")); + jclose(v); + printerrors = 0; +} + +void +threadmain(int argc, char **argv) +{ + char *mtpt, *name; + + mtpt = nil; + name = nil; + ARGBEGIN{ + case 'D': + chatty9p++; + break; + case 'F': + chattyfuse++; + break; + case 'H': + chattyhttp++; + break; + case 'm': + mtpt = EARGF(usage()); + break; + case 's': + name = EARGF(usage()); + break; + case 'k': + keypattern = EARGF(usage()); + break; + default: + usage(); + }ARGEND + + if(argc != 0) + usage(); + + if(name == nil && mtpt == nil) + mtpt = "/n/smug"; + + /* + * Check twice -- if there is an exited smugfs instance + * mounted there, the first access will fail but unmount it. + */ + if(mtpt && access(mtpt, AEXIST) < 0 && access(mtpt, AEXIST) < 0) + sysfatal("mountpoint %s does not exist", mtpt); + + fmtinstall('H', encodefmt); + fmtinstall('[', encodefmt); // base-64 + fmtinstall('J', jsonfmt); + fmtinstall('M', dirmodefmt); + fmtinstall('T', timefmt); + fmtinstall('U', urlencodefmt); + + xinit(); + smuglogin(); + threadpostmountsrv(&xsrv, name, mtpt, 0); + threadexits(nil); +} blob - /dev/null blob + c6b192f796574eac29ed6a2eef38d135828c3225 (mode 644) --- /dev/null +++ src/cmd/smugfs/mkfile @@ -0,0 +1,21 @@ +<$PLAN9/src/mkhdr + +TARG=smugfs + +HFILES=a.h + +OFILES=\ + cache.$O\ + download.$O\ + fs.$O\ + http.$O\ + json.$O\ + jsonrpc.$O\ + log.$O\ + main.$O\ + openssl.$O\ + tcp.$O\ + util.$O\ + +<$PLAN9/src/mkone + blob - /dev/null blob + baccd3acd7eb8db99a4b26aa5f624d0a24eb936c (mode 644) --- /dev/null +++ src/cmd/smugfs/openssl.c @@ -0,0 +1,98 @@ +#include +#include +#include +#include +#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 +}; blob - /dev/null blob + a203ece937e37e8a341a53b7997ed9cc5198fb51 (mode 644) --- /dev/null +++ src/cmd/smugfs/tcp.c @@ -0,0 +1,50 @@ +#include "a.h" + +struct Pfd +{ + int fd; +}; + +static Pfd* +httpconnect(char *host) +{ + char buf[1024]; + Pfd *pfd; + int fd; + + snprint(buf, sizeof buf, "tcp!%s!http", host); + if((fd = dial(buf, nil, nil, nil)) < 0) + return nil; + pfd = emalloc(sizeof *pfd); + pfd->fd = fd; + return pfd; +} + +static void +httpclose(Pfd *pfd) +{ + if(pfd == nil) + return; + close(pfd->fd); + free(pfd); +} + +static int +httpwrite(Pfd *pfd, void *v, int n) +{ + return writen(pfd->fd, v, n); +} + +static int +httpread(Pfd *pfd, void *v, int n) +{ + return read(pfd->fd, v, n); +} + +Protocol http = { + httpconnect, + httpread, + httpwrite, + httpclose, +}; + blob - /dev/null blob + b4a649d83180a51aa45940f38b1c658ce2d26f24 (mode 644) --- /dev/null +++ src/cmd/smugfs/util.c @@ -0,0 +1,81 @@ +#include "a.h" + +void* +emalloc(int n) +{ + void *v; + + v = mallocz(n, 1); + if(v == nil) + sysfatal("out of memory"); + return v; +} + +void* +erealloc(void *v, int n) +{ + v = realloc(v, n); + if(v == nil) + sysfatal("out of memory"); + return v; +} + +char* +estrdup(char *s) +{ + s = strdup(s); + if(s == nil) + sysfatal("out of memory"); + return s; +} + +int +timefmt(Fmt *f) +{ + Tm tm; + vlong ms; + + ms = nsec()/1000000; + + tm = *localtime(ms/1000); + fmtprint(f, "%02d:%02d:%02d.%03d", + tm.hour, tm.min, tm.sec, + (int)(ms%1000)); + return 0; +} + +int +writen(int fd, void *buf, int n) +{ + long m, tot; + + for(tot=0; tot 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; +} +