xref: /plan9/sys/src/cmd/wikifs/fs.c (revision 1052a86abe4783012df9e7959032cbd3f59e6d9c)
1 #include <u.h>
2 #include <libc.h>
3 #include <bio.h>
4 #include <String.h>
5 #include <thread.h>
6 #include "wiki.h"
7 
8 #include <auth.h>
9 #include <fcall.h>
10 #include <9p.h>
11 
12 enum {
13 	Qindexhtml,
14 	Qindextxt,
15 	Qraw,
16 	Qhistoryhtml,
17 	Qhistorytxt,
18 	Qdiffhtml,
19 	Qedithtml,
20 	Qwerrorhtml,
21 	Qwerrortxt,
22 	Qhttplogin,
23 	Nfile,
24 };
25 
26 static char *filelist[] = {
27 	"index.html",
28 	"index.txt",
29 	"current",
30 	"history.html",
31 	"history.txt",
32 	"diff.html",
33 	"edit.html",
34 	"werror.html",
35 	"werror.txt",
36 	".httplogin",
37 };
38 
39 static int needhist[Nfile] = {
40 [Qhistoryhtml] 1,
41 [Qhistorytxt] 1,
42 [Qdiffhtml] 1,
43 };
44 
45 /*
46  * The qids are <8-bit type><16-bit page number><16-bit page version><8-bit file index>.
47  */
48 enum {		/* <8-bit type> */
49 	Droot = 1,
50 	D1st,
51 	D2nd,
52 	Fnew,
53 	Fmap,
54 	F1st,
55 	F2nd,
56 };
57 
58 uvlong
mkqid(int type,int num,int vers,int file)59 mkqid(int type, int num, int vers, int file)
60 {
61 	return ((uvlong)type<<40) | ((uvlong)num<<24) | (vers<<8) | file;
62 }
63 
64 int
qidtype(uvlong path)65 qidtype(uvlong path)
66 {
67 	return (path>>40)&0xFF;
68 }
69 
70 int
qidnum(uvlong path)71 qidnum(uvlong path)
72 {
73 	return (path>>24)&0xFFFF;
74 }
75 
76 int
qidvers(uvlong path)77 qidvers(uvlong path)
78 {
79 	return (path>>8)&0xFFFF;
80 }
81 
82 int
qidfile(uvlong path)83 qidfile(uvlong path)
84 {
85 	return path&0xFF;
86 }
87 
88 typedef struct Aux Aux;
89 struct Aux {
90 	String *name;
91 	Whist *w;
92 	int n;
93 	ulong t;
94 	String *s;
95 	Map *map;
96 };
97 
98 static void
fsattach(Req * r)99 fsattach(Req *r)
100 {
101 	Aux *a;
102 
103 	if(r->ifcall.aname && r->ifcall.aname[0]){
104 		respond(r, "invalid attach specifier");
105 		return;
106 	}
107 
108 	a = emalloc(sizeof(Aux));
109 	r->fid->aux = a;
110 	a->name = s_copy(r->ifcall.uname);
111 
112 	r->ofcall.qid = (Qid){mkqid(Droot, 0, 0, 0), 0, QTDIR};
113 	r->fid->qid = r->ofcall.qid;
114 	respond(r, nil);
115 }
116 
117 static String *
httplogin(void)118 httplogin(void)
119 {
120 	String *s=s_new();
121 	Biobuf *b;
122 
123 	if((b = wBopen(".httplogin", OREAD)) == nil)
124 		goto Return;
125 
126 	while(s_read(b, s, Bsize) > 0)
127 		;
128 	Bterm(b);
129 
130 Return:
131 	return s;
132 }
133 
134 static char*
fswalk1(Fid * fid,char * name,Qid * qid)135 fswalk1(Fid *fid, char *name, Qid *qid)
136 {
137 	char *q;
138 	int i, isdotdot, n, t;
139 	uvlong path;
140 	Aux *a;
141 	Whist *wh;
142 	String *s;
143 
144 	isdotdot = strcmp(name, "..")==0;
145 	n = strtoul(name, &q, 10);
146 	path = fid->qid.path;
147 	a = fid->aux;
148 
149 	switch(qidtype(path)){
150 	case 0:
151 		return "wikifs: bad path in server (bug)";
152 
153 	case Droot:
154 		if(isdotdot){
155 			*qid = fid->qid;
156 			return nil;
157 		}
158 		if(strcmp(name, "new")==0){
159 			*qid = (Qid){mkqid(Fnew, 0, 0, 0), 0, 0};
160 			return nil;
161 		}
162 		if(strcmp(name, "map")==0){
163 			*qid = (Qid){mkqid(Fmap, 0, 0, 0), 0, 0};
164 			return nil;
165 		}
166 		if((*q!='\0' || (wh=getcurrent(n))==nil)
167 		&& (wh=getcurrentbyname(name))==nil)
168 			return "file does not exist";
169 		*qid = (Qid){mkqid(D1st, wh->n, 0, 0), wh->doc->time, QTDIR};
170 		a->w = wh;
171 		return nil;
172 
173 	case D1st:
174 		if(isdotdot){
175 			*qid = (Qid){mkqid(Droot, 0, 0, 0), 0, QTDIR};
176 			return nil;
177 		}
178 
179 		/* handle history directories */
180 		if(*q == '\0'){
181 			if((wh = gethistory(qidnum(path))) == nil)
182 				return "file does not exist";
183 			for(i=0; i<wh->ndoc; i++)
184 				if(wh->doc[i].time == n)
185 					break;
186 			if(i==wh->ndoc){
187 				closewhist(wh);
188 				return "file does not exist";
189 			}
190 			closewhist(a->w);
191 			a->w = wh;
192 			a->n = i;
193 			*qid = (Qid){mkqid(D2nd, qidnum(path), i, 0), wh->doc[i].time, QTDIR};
194 			return nil;
195 		}
196 
197 		/* handle files other than index */
198 		for(i=0; i<nelem(filelist); i++){
199 			if(strcmp(name, filelist[i])==0){
200 				if(needhist[i]){
201 					if((wh = gethistory(qidnum(path))) == nil)
202 						return "file does not exist";
203 					closewhist(a->w);
204 					a->w = wh;
205 				}
206 				*qid = (Qid){mkqid(F1st, qidnum(path), 0, i), a->w->doc->time, 0};
207 				goto Gotfile;
208 			}
209 		}
210 		return "file does not exist";
211 
212 	case D2nd:
213 		if(isdotdot){
214 			/*
215 			 * Can't use a->w[a->ndoc-1] because that
216 			 * might be a failed write rather than the real one.
217 			 */
218 			*qid = (Qid){mkqid(D1st, qidnum(path), 0, 0), 0, QTDIR};
219 			if((wh = getcurrent(qidnum(path))) == nil)
220 				return "file does not exist";
221 			closewhist(a->w);
222 			a->w = wh;
223 			a->n = 0;
224 			return nil;
225 		}
226 		for(i=0; i<=Qraw; i++){
227 			if(strcmp(name, filelist[i])==0){
228 				*qid = (Qid){mkqid(F2nd, qidnum(path), qidvers(path), i), a->w->doc->time, 0};
229 				goto Gotfile;
230 			}
231 		}
232 		return "file does not exist";
233 
234 	default:
235 		return "bad programming";
236 	}
237 	/* not reached */
238 
239 Gotfile:
240 	t = qidtype(qid->path);
241 	switch(qidfile(qid->path)){
242 	case Qindexhtml:
243 		s = tohtml(a->w, a->w->doc+a->n,
244 			t==F1st? Tpage : Toldpage);
245 		break;
246 	case Qindextxt:
247 		s = totext(a->w, a->w->doc+a->n,
248 			t==F1st? Tpage : Toldpage);
249 		break;
250 	case Qraw:
251 		s = s_copy(a->w->title);
252 		s = s_append(s, "\n");
253 		s = doctext(s, &a->w->doc[a->n]);
254 		break;
255 	case Qhistoryhtml:
256 		s = tohtml(a->w, a->w->doc+a->n, Thistory);
257 		break;
258 	case Qhistorytxt:
259 		s = totext(a->w, a->w->doc+a->n, Thistory);
260 		break;
261 	case Qdiffhtml:
262 		s = tohtml(a->w, a->w->doc+a->n, Tdiff);
263 		break;
264 	case Qedithtml:
265 		s = tohtml(a->w, a->w->doc+a->n, Tedit);
266 		break;
267 	case Qwerrorhtml:
268 		s = tohtml(a->w, a->w->doc+a->n, Twerror);
269 		break;
270 	case Qwerrortxt:
271 		s = totext(a->w, a->w->doc+a->n, Twerror);
272 		break;
273 	case Qhttplogin:
274 		s = httplogin();
275 		break;
276 	default:
277 		return "internal error";
278 	}
279 	a->s = s;
280 	return nil;
281 }
282 
283 static void
fsopen(Req * r)284 fsopen(Req *r)
285 {
286 	int t;
287 	uvlong path;
288 	Aux *a;
289 	Fid *fid;
290 	Whist *wh;
291 
292 	fid = r->fid;
293 	path = fid->qid.path;
294 	t = qidtype(fid->qid.path);
295 	if((r->ifcall.mode != OREAD && t != Fnew && t != Fmap)
296 	|| (r->ifcall.mode&ORCLOSE)){
297 		respond(r, "permission denied");
298 		return;
299 	}
300 
301 	a = fid->aux;
302 	switch(t){
303 	case Droot:
304 		currentmap(0);
305 		rlock(&maplock);
306 		a->map = map;
307 		incref(map);
308 		runlock(&maplock);
309 		respond(r, nil);
310 		break;
311 
312 	case D1st:
313 		if((wh = gethistory(qidnum(path))) == nil){
314 			respond(r, "file does not exist");
315 			return;
316 		}
317 		closewhist(a->w);
318 		a->w = wh;
319 		a->n = a->w->ndoc-1;
320 		r->ofcall.qid.vers = wh->doc[a->n].time;
321 		r->fid->qid = r->ofcall.qid;
322 		respond(r, nil);
323 		break;
324 
325 	case D2nd:
326 		respond(r, nil);
327 		break;
328 
329 	case Fnew:
330 		a->s = s_copy("");
331 		respond(r, nil);
332 		break;
333 
334 	case Fmap:
335 	case F1st:
336 	case F2nd:
337 		respond(r, nil);
338 		break;
339 
340 	default:
341 		respond(r, "programmer error");
342 		break;
343 	}
344 }
345 
346 static char*
fsclone(Fid * old,Fid * new)347 fsclone(Fid *old, Fid *new)
348 {
349 	Aux *a;
350 
351 	a = emalloc(sizeof(*a));
352 	*a = *(Aux*)old->aux;
353 	if(a->s)
354 		s_incref(a->s);
355 	if(a->w)
356 		incref(a->w);
357 	if(a->map)
358 		incref(a->map);
359 	if(a->name)
360 		s_incref(a->name);
361 	new->aux = a;
362 	new->qid = old->qid;
363 
364 	return nil;
365 }
366 
367 static void
fsdestroyfid(Fid * fid)368 fsdestroyfid(Fid *fid)
369 {
370 	Aux *a;
371 
372 	a = fid->aux;
373 	if(a==nil)
374 		return;
375 
376 	if(a->name)
377 		s_free(a->name);
378 	if(a->map)
379 		closemap(a->map);
380 	if(a->s)
381 		s_free(a->s);
382 	if(a->w)
383 		closewhist(a->w);
384 	free(a);
385 	fid->aux = nil;
386 }
387 
388 static void
fillstat(Dir * d,uvlong path,ulong tm,ulong length)389 fillstat(Dir *d, uvlong path, ulong tm, ulong length)
390 {
391 	char tmp[32], *p;
392 	int type;
393 
394 	memset(d, 0, sizeof(Dir));
395 	d->uid = estrdup9p("wiki");
396 	d->gid = estrdup9p("wiki");
397 
398 	switch(qidtype(path)){
399 	case Droot:
400 	case D1st:
401 	case D2nd:
402 		type = QTDIR;
403 		break;
404 	default:
405 		type = 0;
406 		break;
407 	}
408 	d->qid = (Qid){path, tm, type};
409 
410 	d->atime = d->mtime = tm;
411 	d->length = length;
412 	if(qidfile(path) == Qedithtml)
413 		d->atime = d->mtime = time(0);
414 
415 	switch(qidtype(path)){
416 	case Droot:
417 		d->name = estrdup("/");
418 		d->mode = DMDIR|0555;
419 		break;
420 
421 	case D1st:
422 		d->name = numtoname(qidnum(path));
423 		if(d->name == nil)
424 			d->name = estrdup("<dead>");
425 		for(p=d->name; *p; p++)
426 			if(*p==' ')
427 				*p = '_';
428 		d->mode = DMDIR|0555;
429 		break;
430 
431 	case D2nd:
432 		snprint(tmp, sizeof tmp, "%lud", tm);
433 		d->name = estrdup(tmp);
434 		d->mode = DMDIR|0555;
435 		break;
436 
437 	case Fmap:
438 		d->name = estrdup("map");
439 		d->mode = 0666;
440 		break;
441 
442 	case Fnew:
443 		d->name = estrdup("new");
444 		d->mode = 0666;
445 		break;
446 
447 	case F1st:
448 		d->name = estrdup(filelist[qidfile(path)]);
449 		d->mode = 0444;
450 		break;
451 
452 	case F2nd:
453 		d->name = estrdup(filelist[qidfile(path)]);
454 		d->mode = 0444;
455 		break;
456 
457 	default:
458 		print("bad qid path 0x%.8llux\n", path);
459 		break;
460 	}
461 }
462 
463 static void
fsstat(Req * r)464 fsstat(Req *r)
465 {
466 	Aux *a;
467 	Fid *fid;
468 	ulong t;
469 
470 	t = 0;
471 	fid = r->fid;
472 	if((a = fid->aux) && a->w)
473 		t = a->w->doc[a->n].time;
474 
475 	fillstat(&r->d, fid->qid.path, t, a->s ? s_len(a->s) : 0);
476 	respond(r, nil);
477 }
478 
479 typedef struct Bogus Bogus;
480 struct Bogus {
481 	uvlong path;
482 	Aux *a;
483 };
484 
485 static int
rootgen(int i,Dir * d,void * aux)486 rootgen(int i, Dir *d, void *aux)
487 {
488 	Aux *a;
489 	Bogus *b;
490 
491 	b = aux;
492 	a = b->a;
493 	switch(i){
494 	case 0:	/* new */
495 		fillstat(d, mkqid(Fnew, 0, 0, 0), a->map->t, 0);
496 		return 0;
497 	case 1:	/* map */
498 		fillstat(d, mkqid(Fmap, 0, 0, 0), a->map->t, 0);
499 		return 0;
500 	default:	/* first-level directory */
501 		i -= 2;
502 		if(i >= a->map->nel)
503 			return -1;
504 		fillstat(d, mkqid(D1st, a->map->el[i].n, 0, 0), a->map->t, 0);
505 		return 0;
506 	}
507 }
508 
509 static int
firstgen(int i,Dir * d,void * aux)510 firstgen(int i, Dir *d, void *aux)
511 {
512 	ulong t;
513 	Bogus *b;
514 	int num;
515 	Aux *a;
516 
517 	b = aux;
518 	num = qidnum(b->path);
519 	a = b->a;
520 	t = a->w->doc[a->n].time;
521 
522 	if(i < Nfile){	/* file in first-level directory */
523 		fillstat(d, mkqid(F1st, num, 0, i), t, 0);
524 		return 0;
525 	}
526 	i -= Nfile;
527 
528 	if(i < a->w->ndoc){	/* second-level (history) directory */
529 		fillstat(d, mkqid(D2nd, num, i, 0), a->w->doc[i].time, 0);
530 		return 0;
531 	}
532 	//i -= a->w->ndoc;
533 
534 	return -1;
535 }
536 
537 static int
secondgen(int i,Dir * d,void * aux)538 secondgen(int i, Dir *d, void *aux)
539 {
540 	Bogus *b;
541 	uvlong path;
542 	Aux *a;
543 
544 	b = aux;
545 	path = b->path;
546 	a = b->a;
547 
548 	if(i <= Qraw){	/* index.html, index.txt, raw */
549 		fillstat(d, mkqid(F2nd, qidnum(path), qidvers(path), i), a->w->doc[a->n].time, 0);
550 		return 0;
551 	}
552 	//i -= Qraw;
553 
554 	return -1;
555 }
556 
557 static void
fsread(Req * r)558 fsread(Req *r)
559 {
560 	char *t, *s;
561 	uvlong path;
562 	Aux *a;
563 	Bogus b;
564 
565 	a = r->fid->aux;
566 	path = r->fid->qid.path;
567 	b.a = a;
568 	b.path = path;
569 	switch(qidtype(path)){
570 	default:
571 		respond(r, "cannot happen (bad qid)");
572 		return;
573 
574 	case Droot:
575 		if(a == nil || a->map == nil){
576 			respond(r, "cannot happen (no map)");
577 			return;
578 		}
579 		dirread9p(r, rootgen, &b);
580 		respond(r, nil);
581 		return;
582 
583 	case D1st:
584 		if(a == nil || a->w == nil){
585 			respond(r, "cannot happen (no wh)");
586 			return;
587 		}
588 		dirread9p(r, firstgen, &b);
589 		respond(r, nil);
590 		return;
591 
592 	case D2nd:
593 		dirread9p(r, secondgen, &b);
594 		respond(r, nil);
595 		return;
596 
597 	case Fnew:
598 		if(a->s){
599 			respond(r, "protocol botch");
600 			return;
601 		}
602 		/* fall through */
603 	case Fmap:
604 		t = numtoname(a->n);
605 		if(t == nil){
606 			respond(r, "unknown name");
607 			return;
608 		}
609 		for(s=t; *s; s++)
610 			if(*s == ' ')
611 				*s = '_';
612 		readstr(r, t);
613 		free(t);
614 		respond(r, nil);
615 		return;
616 
617 	case F1st:
618 	case F2nd:
619 		if(a == nil || a->s == nil){
620 			respond(r, "cannot happen (no s)");
621 			return;
622 		}
623 		readbuf(r, s_to_c(a->s), s_len(a->s));
624 		respond(r, nil);
625 		return;
626 	}
627 }
628 
629 typedef struct Sread Sread;
630 struct Sread {
631 	char *rp;
632 };
633 
634 static char*
Srdline(void * v,int c)635 Srdline(void *v, int c)
636 {
637 	char *p, *rv;
638 	Sread *s;
639 
640 	s = v;
641 	if(s->rp == nil)
642 		rv = nil;
643 	else if(p = strchr(s->rp, c)){
644 		*p = '\0';
645 		rv = s->rp;
646 		s->rp = p+1;
647 	}else{
648 		rv = s->rp;
649 		s->rp = nil;
650 	}
651 	return rv;
652 }
653 
654 static void
responderrstr(Req * r)655 responderrstr(Req *r)
656 {
657 	char buf[ERRMAX];
658 
659 	rerrstr(buf, sizeof buf);
660 	if(buf[0] == '\0')
661 		strcpy(buf, "unknown error");
662 	respond(r, buf);
663 }
664 
665 static void
fswrite(Req * r)666 fswrite(Req *r)
667 {
668 	char *author, *comment, *net, *err, *p, *title, tmp[40];
669 	int rv, n;
670 	ulong t;
671 	Aux *a;
672 	Fid *fid;
673 	Sread s;
674 	String *stmp;
675 	Whist *w;
676 
677 	fid = r->fid;
678 	a = fid->aux;
679 	switch(qidtype(fid->qid.path)){
680 	case Fmap:
681 		stmp = s_nappend(s_reset(nil), r->ifcall.data, r->ifcall.count);
682 		a->n = nametonum(s_to_c(stmp));
683 		s_free(stmp);
684 		if(a->n < 0)
685 			respond(r, "name not found");
686 		else
687 			respond(r, nil);
688 		return;
689 	case Fnew:
690 		break;
691 	default:
692 		respond(r, "cannot happen");
693 		return;
694 	}
695 
696 	if(a->s == nil){
697 		respond(r, "protocol botch");
698 		return;
699 	}
700 	if(r->ifcall.count==0){	/* do final processing */
701 		s.rp = s_to_c(a->s);
702 		w = nil;
703 		err = "bad format";
704 		if((title = Srdline(&s, '\n')) == nil){
705 		Error:
706 			if(w)
707 				closewhist(w);
708 			s_free(a->s);
709 			a->s = nil;
710 			respond(r, err);
711 			return;
712 		}
713 
714 		w = emalloc(sizeof(*w));
715 		incref(w);
716 		w->title = estrdup(title);
717 
718 		t = 0;
719 		author = estrdup(s_to_c(a->name));
720 
721 		comment = nil;
722 		while(s.rp && *s.rp && *s.rp != '\n'){
723 			p = Srdline(&s, '\n');
724 			assert(p != nil);
725 			switch(p[0]){
726 			case 'A':
727 				free(author);
728 				author = estrdup(p+1);
729 				break;
730 			case 'D':
731 				t = strtoul(p+1, &p, 10);
732 				if(*p != '\0')
733 					goto Error;
734 				break;
735 			case 'C':
736 				free(comment);
737 				comment = estrdup(p+1);
738 				break;
739 			}
740 		}
741 
742 		w->doc = emalloc(sizeof(w->doc[0]));
743 		w->doc->time = time(0);
744 		w->doc->comment = comment;
745 
746 		if(net = r->pool->srv->aux){
747 			p = emalloc(strlen(author)+10+strlen(net));
748 			strcpy(p, author);
749 			strcat(p, " (");
750 			strcat(p, net);
751 			strcat(p, ")");
752 			free(author);
753 			author = p;
754 		}
755 		w->doc->author = author;
756 
757 		if((w->doc->wtxt = Brdpage(Srdline, &s)) == nil){
758 			err = "empty document";
759 			goto Error;
760 		}
761 
762 		w->ndoc = 1;
763 		if((n = allocnum(w->title, 0)) < 0)
764 			goto Error;
765 		sprint(tmp, "D%lud\n", w->doc->time);
766 		a->s = s_reset(a->s);
767 		a->s = doctext(a->s, w->doc);
768 		rv = writepage(n, t, a->s, w->title);
769 		s_free(a->s);
770 		a->s = nil;
771 		a->n = n;
772 		closewhist(w);
773 		if(rv < 0)
774 			responderrstr(r);
775 		else
776 			respond(r, nil);
777 		return;
778 	}
779 
780 	if(s_len(a->s)+r->ifcall.count > Maxfile){
781 		respond(r, "file too large");
782 		s_free(a->s);
783 		a->s = nil;
784 		return;
785 	}
786 	a->s = s_nappend(a->s, r->ifcall.data, r->ifcall.count);
787 	r->ofcall.count = r->ifcall.count;
788 	respond(r, nil);
789 }
790 
791 Srv wikisrv = {
792 .attach=	fsattach,
793 .destroyfid=	fsdestroyfid,
794 .clone=	fsclone,
795 .walk1=	fswalk1,
796 .open=	fsopen,
797 .read=	fsread,
798 .write=	fswrite,
799 .stat=	fsstat,
800 };
801 
802 void
usage(void)803 usage(void)
804 {
805 	fprint(2, "usage: wikifs [-D] [-a addr]... [-m mtpt] [-p perm] [-s service] dir\n");
806 	exits("usage");
807 }
808 
809 void
main(int argc,char ** argv)810 main(int argc, char **argv)
811 {
812 	char **addr;
813 	int i, naddr;
814 	char *buf;
815 	char *service, *mtpt;
816 	ulong perm;
817 	Dir d, *dp;
818 	Srv *s;
819 
820 	naddr = 0;
821 	addr = nil;
822 	perm = 0;
823 	service = nil;
824 	mtpt = "/mnt/wiki";
825 	ARGBEGIN{
826 	case 'D':
827 		chatty9p++;
828 		break;
829 	case 'a':
830 		if(naddr%8 == 0)
831 			addr = erealloc(addr, (naddr+8)*sizeof(addr[0]));
832 		addr[naddr++] = EARGF(usage());
833 		break;
834 	case 'm':
835 		mtpt = EARGF(usage());
836 		break;
837 	case 'M':
838 		mtpt = nil;
839 		break;
840 	case 'p':
841 		perm = strtoul(EARGF(usage()), nil, 8);
842 		break;
843 	case 's':
844 		service = EARGF(usage());
845 		break;
846 	default:
847 		usage();
848 		break;
849 	}ARGEND
850 
851 	if(argc != 1)
852 		usage();
853 
854 	if((dp = dirstat(argv[0])) == nil)
855 		sysfatal("dirstat %s: %r", argv[0]);
856 	if((dp->mode&DMDIR) == 0)
857 		sysfatal("%s: not a directory", argv[0]);
858 	free(dp);
859 	wikidir = argv[0];
860 
861 	currentmap(0);
862 
863 	for(i=0; i<naddr; i++)
864 		listensrv(&wikisrv, addr[i]);
865 
866 	s = emalloc(sizeof *s);
867 	*s = wikisrv;
868 	postmountsrv(s, service, mtpt, MREPL|MCREATE);
869 	if(perm){
870 		buf = emalloc9p(5+strlen(service)+1);
871 		strcpy(buf, "/srv/");
872 		strcat(buf, service);
873 		nulldir(&d);
874 		d.mode = perm;
875 		if(dirwstat(buf, &d) < 0)
876 			fprint(2, "wstat: %r\n");
877 		free(buf);
878 	}
879 	exits(nil);
880 }
881