701 lines
12 KiB
C
701 lines
12 KiB
C
/*
|
|
* I/O for a Wiki document set.
|
|
*
|
|
* The files are kept in one flat directory.
|
|
* There are three files for each document:
|
|
* nnn - current version of the document
|
|
* nnn.hist - history (all old versions) of the document
|
|
* append-only
|
|
* L.nnn - write lock file for the document
|
|
*
|
|
* At the moment, since we don't have read/write locks
|
|
* in the file system, we use the L.nnn file as a read lock too.
|
|
* It's a hack but there aren't supposed to be many readers
|
|
* anyway.
|
|
*
|
|
* The nnn.hist file is in the format read by Brdwhist.
|
|
* The nnn file is in that format too, but only contains the
|
|
* last entry of the nnn.hist file.
|
|
*
|
|
* In addition to this set of files, there is an append-only
|
|
* map file that provides a mapping between numbers and titles.
|
|
* The map file is a sequence of lines of the form
|
|
* nnn Title Here
|
|
* The lock file L.map must be held to add to the end, to
|
|
* make sure that the numbers are allocated sequentially.
|
|
*
|
|
* We assume that writes to the map file will fit in one message,
|
|
* so that we don't have to read-lock the file.
|
|
*/
|
|
|
|
#include <u.h>
|
|
#include <libc.h>
|
|
#include <bio.h>
|
|
#include <String.h>
|
|
#include <thread.h>
|
|
#include "wiki.h"
|
|
|
|
enum {
|
|
Nhash = 64,
|
|
Mcache = 128,
|
|
};
|
|
|
|
typedef struct Wcache Wcache;
|
|
struct Wcache {
|
|
int n;
|
|
ulong use;
|
|
RWLock;
|
|
ulong tcurrent;
|
|
ulong thist;
|
|
Whist *hist;
|
|
Whist *current;
|
|
Qid qid;
|
|
Qid qidhist;
|
|
Wcache *hash;
|
|
};
|
|
|
|
static RWLock cachelock;
|
|
static Wcache *tab[Nhash];
|
|
int ncache;
|
|
|
|
void
|
|
closewhist(Whist *wh)
|
|
{
|
|
int i;
|
|
|
|
if(wh && decref(wh) == 0){
|
|
free(wh->title);
|
|
for(i=0; i<wh->ndoc; i++){
|
|
free(wh->doc[i].author);
|
|
free(wh->doc[i].comment);
|
|
freepage(wh->doc[i].wtxt);
|
|
}
|
|
free(wh->doc);
|
|
free(wh);
|
|
}
|
|
}
|
|
|
|
void
|
|
freepage(Wpage *p)
|
|
{
|
|
Wpage *next;
|
|
|
|
for(; p; p=next){
|
|
next = p->next;
|
|
free(p->text);
|
|
free(p->url);
|
|
free(p);
|
|
}
|
|
}
|
|
|
|
static Wcache*
|
|
findcache(int n)
|
|
{
|
|
Wcache *w;
|
|
|
|
for(w=tab[n%Nhash]; w; w=w->hash)
|
|
if(w->n == n){
|
|
w->use = time(0);
|
|
return w;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
static int
|
|
getlock(char *lock)
|
|
{
|
|
char buf[ERRMAX];
|
|
int i, fd;
|
|
enum { SECS = 200 };
|
|
|
|
for(i=0; i<SECS*10; i++){
|
|
fd = wcreate(lock, ORDWR, DMEXCL|0666);
|
|
if(fd >= 0)
|
|
return fd;
|
|
buf[0] = '\0';
|
|
rerrstr(buf, sizeof buf);
|
|
if(strstr(buf, "locked") == nil)
|
|
break;
|
|
sleep(1000/10);
|
|
}
|
|
werrstr("couldn't acquire lock %s: %r", lock);
|
|
return -1;
|
|
}
|
|
|
|
static Whist*
|
|
readwhist(char *file, char *lock, Qid *qid)
|
|
{
|
|
int lfd;
|
|
Biobuf *b;
|
|
Dir *d;
|
|
Whist *wh;
|
|
|
|
if((lfd=getlock(lock)) < 0) // LOG?
|
|
return nil;
|
|
|
|
if(qid){
|
|
if((d = wdirstat(file)) == nil){
|
|
close(lfd);
|
|
return nil;
|
|
}
|
|
*qid = d->qid;
|
|
free(d);
|
|
}
|
|
|
|
if((b = wBopen(file, OREAD)) == nil){ //LOG?
|
|
close(lfd);
|
|
return nil;
|
|
}
|
|
|
|
wh = Brdwhist(b);
|
|
|
|
Bterm(b);
|
|
close(lfd);
|
|
return wh;
|
|
}
|
|
|
|
static void
|
|
gencurrent(Wcache *w, Qid *q, char *file, char *lock, ulong *t, Whist **wp, int n)
|
|
{
|
|
Dir *d;
|
|
Whist *wh;
|
|
|
|
if(*wp && *t+Tcache >= time(0))
|
|
return;
|
|
|
|
wlock(w);
|
|
if(*wp && *t+Tcache >= time(0)){
|
|
wunlock(w);
|
|
return;
|
|
}
|
|
|
|
if(((d = wdirstat(file)) == nil) || (d->qid.path==q->path && d->qid.vers==q->vers)){
|
|
*t = time(0);
|
|
wunlock(w);
|
|
free(d);
|
|
return;
|
|
}
|
|
|
|
free(d);
|
|
if(wh = readwhist(file, lock, q)){
|
|
wh->n = n;
|
|
*t = time(0);
|
|
closewhist(*wp);
|
|
*wp = wh;
|
|
}
|
|
else fprint(2, "error file=%s lock=%s %r\n", file, lock);
|
|
wunlock(w);
|
|
}
|
|
|
|
static void
|
|
current(Wcache *w)
|
|
{
|
|
char tmp[40];
|
|
char tmplock[40];
|
|
|
|
sprint(tmplock, "d/L.%d", w->n);
|
|
sprint(tmp, "d/%d", w->n);
|
|
gencurrent(w, &w->qid, tmp, tmplock, &w->tcurrent, &w->current, w->n);
|
|
}
|
|
|
|
static void
|
|
currenthist(Wcache *w)
|
|
{
|
|
char hist[40], lock[40];
|
|
|
|
sprint(hist, "d/%d.hist", w->n);
|
|
sprint(lock, "d/L.%d", w->n);
|
|
|
|
gencurrent(w, &w->qidhist, hist, lock, &w->thist, &w->hist, w->n);
|
|
}
|
|
|
|
void
|
|
voidcache(int n)
|
|
{
|
|
Wcache *c;
|
|
|
|
rlock(&cachelock);
|
|
if(c = findcache(n)){
|
|
wlock(c);
|
|
c->tcurrent = 0;
|
|
c->thist = 0;
|
|
/* aggressively free memory */
|
|
closewhist(c->hist);
|
|
c->hist = nil;
|
|
closewhist(c->current);
|
|
c->current = nil;
|
|
wunlock(c);
|
|
}
|
|
runlock(&cachelock);
|
|
}
|
|
|
|
static Whist*
|
|
getcache(int n, int hist)
|
|
{
|
|
int i, isw;
|
|
ulong t;
|
|
Wcache *c, **cp, **evict;
|
|
Whist *wh;
|
|
|
|
isw = 0;
|
|
rlock(&cachelock);
|
|
if(c = findcache(n)){
|
|
Found:
|
|
current(c);
|
|
if(hist)
|
|
currenthist(c);
|
|
rlock(c);
|
|
if(hist)
|
|
wh = c->hist;
|
|
else
|
|
wh = c->current;
|
|
if(wh)
|
|
incref(wh);
|
|
runlock(c);
|
|
if(isw)
|
|
wunlock(&cachelock);
|
|
else
|
|
runlock(&cachelock);
|
|
return wh;
|
|
}
|
|
runlock(&cachelock);
|
|
|
|
wlock(&cachelock);
|
|
if(c = findcache(n)){
|
|
isw = 1; /* better to downgrade lock but can't */
|
|
goto Found;
|
|
}
|
|
|
|
if(ncache < Mcache){
|
|
Alloc:
|
|
c = emalloc(sizeof *c);
|
|
ncache++;
|
|
}else{
|
|
/* find something to evict. */
|
|
t = ~0;
|
|
evict = nil;
|
|
for(i=0; i<Nhash; i++){
|
|
for(cp=&tab[i], c=*cp; c; cp=&c->hash, c=*cp){
|
|
if(c->use < t
|
|
&& (!c->hist || c->hist->ref==1)
|
|
&& (!c->current || c->current->ref==1)){
|
|
evict = cp;
|
|
t = c->use;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(evict == nil){
|
|
fprint(2, "wikifs: nothing to evict\n");
|
|
goto Alloc;
|
|
}
|
|
|
|
c = *evict;
|
|
*evict = c->hash;
|
|
|
|
closewhist(c->current);
|
|
closewhist(c->hist);
|
|
memset(c, 0, sizeof *c);
|
|
}
|
|
|
|
c->n = n;
|
|
c->hash = tab[n%Nhash];
|
|
tab[n%Nhash] = c;
|
|
isw = 1;
|
|
goto Found;
|
|
}
|
|
|
|
Whist*
|
|
getcurrent(int n)
|
|
{
|
|
return getcache(n, 0);
|
|
}
|
|
|
|
Whist*
|
|
gethistory(int n)
|
|
{
|
|
return getcache(n, 1);
|
|
}
|
|
|
|
RWLock maplock;
|
|
Map *map;
|
|
|
|
static int
|
|
mapcmp(const void *va, const void *vb)
|
|
{
|
|
Mapel *a, *b;
|
|
|
|
a = (Mapel*)va;
|
|
b = (Mapel*)vb;
|
|
|
|
return strcmp(a->s, b->s);
|
|
}
|
|
|
|
void
|
|
closemap(Map *m)
|
|
{
|
|
if(decref(m)==0){
|
|
free(m->buf);
|
|
free(m->el);
|
|
free(m);
|
|
}
|
|
}
|
|
|
|
void
|
|
currentmap(int force)
|
|
{
|
|
char *p, *q, *r;
|
|
int lfd, fd, m, n;
|
|
Dir *d;
|
|
Map *nmap;
|
|
char *err = nil;
|
|
|
|
lfd = -1;
|
|
fd = -1;
|
|
d = nil;
|
|
nmap = nil;
|
|
if(!force && map && map->t+Tcache >= time(0))
|
|
return;
|
|
|
|
wlock(&maplock);
|
|
if(!force && map && map->t+Tcache >= time(0))
|
|
goto Return;
|
|
|
|
if((lfd = getlock("d/L.map")) < 0){
|
|
err = "can't lock";
|
|
goto Return;
|
|
}
|
|
|
|
if((d = wdirstat("d/map")) == nil)
|
|
goto Return;
|
|
|
|
if(map && d->qid.path == map->qid.path && d->qid.vers == map->qid.vers){
|
|
map->t = time(0);
|
|
goto Return;
|
|
}
|
|
|
|
if(d->length > Maxmap){
|
|
//LOG
|
|
err = "too long";
|
|
goto Return;
|
|
}
|
|
|
|
if((fd = wopen("d/map", OREAD)) < 0)
|
|
goto Return;
|
|
|
|
nmap = emalloc(sizeof *nmap);
|
|
nmap->buf = emalloc(d->length+1);
|
|
n = readn(fd, nmap->buf, d->length);
|
|
if(n != d->length){
|
|
err = "bad length";
|
|
goto Return;
|
|
}
|
|
nmap->buf[n] = '\0';
|
|
|
|
n = 0;
|
|
for(p=nmap->buf; p; p=strchr(p+1, '\n'))
|
|
n++;
|
|
nmap->el = emalloc(n*sizeof(nmap->el[0]));
|
|
|
|
m = 0;
|
|
for(p=nmap->buf; p && *p && m < n; p=q){
|
|
if(q = strchr(p+1, '\n'))
|
|
*q++ = '\0';
|
|
nmap->el[m].n = strtol(p, &r, 10);
|
|
if(*r == ' ')
|
|
r++;
|
|
else
|
|
{}//LOG?
|
|
nmap->el[m].s = strcondense(r, 1);
|
|
m++;
|
|
}
|
|
//LOG if m != n
|
|
|
|
nmap->qid = d->qid;
|
|
nmap->t = time(0);
|
|
nmap->nel = m;
|
|
qsort(nmap->el, nmap->nel, sizeof(nmap->el[0]), mapcmp);
|
|
if(map)
|
|
closemap(map);
|
|
map = nmap;
|
|
incref(map);
|
|
nmap = nil;
|
|
|
|
Return:
|
|
free(d);
|
|
if(nmap){
|
|
free(nmap->el);
|
|
free(nmap->buf);
|
|
free(nmap);
|
|
}
|
|
if(map == nil)
|
|
sysfatal("cannot get map: %s: %r", err);
|
|
|
|
if(fd >= 0)
|
|
close(fd);
|
|
if(lfd >= 0)
|
|
close(lfd);
|
|
wunlock(&maplock);
|
|
}
|
|
|
|
int
|
|
allocnum(char *title, int mustbenew)
|
|
{
|
|
char *p, *q;
|
|
int lfd, fd, n;
|
|
Biobuf b;
|
|
|
|
if(strcmp(title, "map")==0 || strcmp(title, "new")==0){
|
|
werrstr("reserved title name");
|
|
return -1;
|
|
}
|
|
|
|
if(title[0]=='\0' || strpbrk(title, "/<>:?")){
|
|
werrstr("invalid character in name");
|
|
return -1;
|
|
}
|
|
if((n = nametonum(title)) >= 0){
|
|
if(mustbenew){
|
|
werrstr("duplicate title");
|
|
return -1;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
title = estrdup(title);
|
|
strcondense(title, 1);
|
|
strlower(title);
|
|
if(strchr(title, '\n') || strlen(title) > 200){
|
|
werrstr("bad title");
|
|
free(title);
|
|
return -1;
|
|
}
|
|
|
|
if((lfd = getlock("d/L.map")) < 0){
|
|
free(title);
|
|
return -1;
|
|
}
|
|
|
|
if((fd = wopen("d/map", ORDWR)) < 0){ // LOG?
|
|
close(lfd);
|
|
free(title);
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* What we really need to do here is make sure the
|
|
* map is up-to-date, then make sure the title isn't
|
|
* taken, and then add it, all without dropping the locks.
|
|
*
|
|
* This turns out to be a mess when you start adding
|
|
* all the necessary dolock flags, so instead we just
|
|
* read through the file ourselves, and let our
|
|
* map catch up on its own.
|
|
*/
|
|
Binit(&b, fd, OREAD);
|
|
n = 0;
|
|
while(p = Brdline(&b, '\n')){
|
|
p[Blinelen(&b)-1] = '\0';
|
|
n = atoi(p)+1;
|
|
q = strchr(p, ' ');
|
|
if(q == nil)
|
|
continue;
|
|
if(strcmp(q+1, title) == 0){
|
|
free(title);
|
|
close(fd);
|
|
close(lfd);
|
|
if(mustbenew){
|
|
werrstr("duplicate title");
|
|
return -1;
|
|
}else
|
|
return n;
|
|
}
|
|
}
|
|
|
|
seek(fd, 0, 2); /* just in case it's not append only */
|
|
fprint(fd, "%d %s\n", n, title);
|
|
close(fd);
|
|
close(lfd);
|
|
free(title);
|
|
/* kick the map */
|
|
currentmap(1);
|
|
return n;
|
|
}
|
|
|
|
int
|
|
nametonum(char *s)
|
|
{
|
|
char *p;
|
|
int i, lo, hi, m, rv;
|
|
|
|
s = estrdup(s);
|
|
strlower(s);
|
|
for(p=s; *p; p++)
|
|
if(*p=='_')
|
|
*p = ' ';
|
|
|
|
currentmap(0);
|
|
rlock(&maplock);
|
|
lo = 0;
|
|
hi = map->nel;
|
|
while(hi-lo > 1){
|
|
m = (lo+hi)/2;
|
|
i = strcmp(s, map->el[m].s);
|
|
if(i < 0)
|
|
hi = m;
|
|
else
|
|
lo = m;
|
|
}
|
|
if(hi-lo == 1 && strcmp(s, map->el[lo].s)==0)
|
|
rv = map->el[lo].n;
|
|
else
|
|
rv = -1;
|
|
runlock(&maplock);
|
|
free(s);
|
|
return rv;
|
|
}
|
|
|
|
char*
|
|
numtoname(int n)
|
|
{
|
|
int i;
|
|
char *s;
|
|
|
|
currentmap(0);
|
|
rlock(&maplock);
|
|
for(i=0; i<map->nel; i++){
|
|
if(map->el[i].n==n)
|
|
break;
|
|
}
|
|
if(i==map->nel){
|
|
runlock(&maplock);
|
|
return nil;
|
|
}
|
|
s = estrdup(map->el[i].s);
|
|
runlock(&maplock);
|
|
return s;
|
|
}
|
|
|
|
Whist*
|
|
getcurrentbyname(char *s)
|
|
{
|
|
int n;
|
|
|
|
if((n = nametonum(s)) < 0)
|
|
return nil;
|
|
return getcache(n, 0);
|
|
}
|
|
|
|
static String*
|
|
Brdstring(Biobuf *b)
|
|
{
|
|
long len;
|
|
String *s;
|
|
Dir *d;
|
|
|
|
d = dirfstat(Bfildes(b));
|
|
if (d == nil) /* shouldn't happen, we just opened it */
|
|
len = 0;
|
|
else
|
|
len = d->length;
|
|
free(d);
|
|
s = s_newalloc(len);
|
|
s_read(b, s, len);
|
|
return s;
|
|
}
|
|
|
|
/*
|
|
* Attempt to install a new page. If t==0 we are creating.
|
|
* Otherwise, we are editing and t must be set to the current
|
|
* version (t is the version we started with) to avoid conflicting
|
|
* writes.
|
|
*
|
|
* If there is a conflicting write, we still write the page to
|
|
* the history file, but mark it as a failed write.
|
|
*/
|
|
int
|
|
writepage(int num, ulong t, String *s, char *title)
|
|
{
|
|
char tmp[40], tmplock[40], err[ERRMAX], hist[40], *p;
|
|
int conflict, lfd, fd;
|
|
Biobuf *b;
|
|
String *os;
|
|
|
|
sprint(tmp, "d/%d", num);
|
|
sprint(tmplock, "d/L.%d", num);
|
|
sprint(hist, "d/%d.hist", num);
|
|
if((lfd = getlock(tmplock)) < 0)
|
|
return -1;
|
|
|
|
conflict = 0;
|
|
if(b = wBopen(tmp, OREAD)){
|
|
Brdline(b, '\n'); /* title */
|
|
if(p = Brdline(b, '\n')) /* version */
|
|
p[Blinelen(b)-1] = '\0';
|
|
if(p==nil || p[0] != 'D'){
|
|
snprint(err, sizeof err, "bad format in extant file");
|
|
conflict = 1;
|
|
}else if(strtoul(p+1, 0, 0) != t){
|
|
os = Brdstring(b); /* why read the whole file? */
|
|
p = strchr(s_to_c(s), '\n');
|
|
if(p!=nil && strcmp(p+1, s_to_c(os))==0){ /* ignore dup write */
|
|
close(lfd);
|
|
s_free(os);
|
|
Bterm(b);
|
|
return 0;
|
|
}
|
|
s_free(os);
|
|
snprint(err, sizeof err, "update conflict %lud != %s", t, p+1);
|
|
conflict = 1;
|
|
}
|
|
Bterm(b);
|
|
}else{
|
|
if(t != 0){
|
|
close(lfd);
|
|
werrstr("did not expect to create");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if((fd = wopen(hist, OWRITE)) < 0){
|
|
if((fd = wcreate(hist, OWRITE, 0666)) < 0){
|
|
close(lfd);
|
|
return -1;
|
|
}else
|
|
fprint(fd, "%s\n", title);
|
|
}
|
|
if(seek(fd, 0, 2) < 0
|
|
|| (conflict && write(fd, "X\n", 2) != 2)
|
|
|| write(fd, s_to_c(s), s_len(s)) != s_len(s)){
|
|
close(fd);
|
|
close(lfd);
|
|
return -1;
|
|
}
|
|
close(fd);
|
|
|
|
if(conflict){
|
|
close(lfd);
|
|
voidcache(num);
|
|
errstr(err, sizeof err);
|
|
return -1;
|
|
}
|
|
|
|
if((fd = wcreate(tmp, OWRITE, 0666)) < 0){
|
|
close(lfd);
|
|
voidcache(num);
|
|
return -1;
|
|
}
|
|
if(write(fd, title, strlen(title)) != strlen(title)
|
|
|| write(fd, "\n", 1) != 1
|
|
|| write(fd, s_to_c(s), s_len(s)) != s_len(s)){
|
|
close(fd);
|
|
close(lfd);
|
|
voidcache(num);
|
|
return -1;
|
|
}
|
|
close(fd);
|
|
close(lfd);
|
|
voidcache(num);
|
|
return 0;
|
|
}
|