ref: babf901b4a508c3ec5d1f89655f10377bbdf9637
dir: /appl/cmd/dbfs.b/
implement Dbfs;
#
# Copyright © 1999 Vita Nuova Limited. All rights reserved.
# Revisions copyright © 2002 Vita Nuova Holdings Limited. All rights reserved.
#
include "sys.m";
sys: Sys;
Qid: import Sys;
include "draw.m";
include "arg.m";
include "styx.m";
styx: Styx;
Tmsg, Rmsg: import styx;
include "styxservers.m";
styxservers: Styxservers;
Fid, Styxserver, Navigator, Navop: import styxservers;
Enotfound, Eperm, Ebadarg: import styxservers;
include "bufio.m";
bufio: Bufio;
Iobuf: import bufio;
Record: adt {
id: int; # file number in directory
x: int; # index in file
dirty: int; # modified but not written
vers: int; # version
data: array of byte;
new: fn(x: array of byte): ref Record;
print: fn(r: self ref Record, fd: ref Sys->FD);
qid: fn(r: self ref Record): Sys->Qid;
};
Database: adt {
name: string;
file: ref Iobuf;
records: array of ref Record;
dirty: int;
vers: int;
nextid: int;
findrec: fn(db: self ref Database, id: int): ref Record;
};
Dbfs: module
{
init: fn(nil: ref Draw->Context, nil: list of string);
};
Qdir, Qnew, Qdata: con iota;
clockfd: ref Sys->FD;
stderr: ref Sys->FD;
database: ref Database;
user: string;
Eremoved: con "file removed";
usage()
{
sys->fprint(stderr, "Usage: dbfs [-a|-b|-ac|-bc] [-D] file mountpoint\n");
raise "fail:usage";
}
nomod(s: string)
{
sys->fprint(stderr, "dbfs: can't load %s: %r\n", s);
raise "fail:load";
}
init(nil: ref Draw->Context, args: list of string)
{
sys = load Sys Sys->PATH;
sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
stderr = sys->fildes(2);
styx = load Styx Styx->PATH;
if(styx == nil)
nomod(Styx->PATH);
styx->init();
styxservers = load Styxservers Styxservers->PATH;
if(styxservers == nil)
nomod(Styxservers->PATH);
styxservers->init(styx);
bufio = load Bufio Bufio->PATH;
if(bufio == nil)
nomod(Bufio->PATH);
arg := load Arg Arg->PATH;
if(arg == nil)
nomod(Arg->PATH);
arg->init(args);
flags := Sys->MREPL;
copt := 0;
empty := 0;
while((o := arg->opt()) != 0)
case o {
'a' => flags = Sys->MAFTER;
'b' => flags = Sys->MBEFORE;
'c' => copt = 1;
'e' => empty = 1;
'D' => styxservers->traceset(1);
* => usage();
}
args = arg->argv();
arg = nil;
if(len args != 2)
usage();
if(copt)
flags |= Sys->MCREATE;
file := hd args;
args = tl args;
mountpt := hd args;
df := bufio->open(file, Sys->OREAD);
if(df == nil && empty){
(rc, nil) := sys->stat(file);
if(rc < 0)
df = bufio->create(file, Sys->OREAD, 8r600);
}
if(df == nil){
sys->fprint(stderr, "dbfs: can't open %s: %r\n", file);
raise "fail:open";
}
(db, err) := dbread(ref Database(file, df, nil, 0, 0, 0));
if(db == nil){
sys->fprint(stderr, "dbfs: can't read %s: %s\n", file, err);
raise "fail:dbread";
}
db.file = nil;
# dbprint(db);
database = db;
sys->pctl(Sys->FORKFD, nil);
user = rf("/dev/user");
if(user == nil)
user = "inferno";
fds := array[2] of ref Sys->FD;
if(sys->pipe(fds) < 0){
sys->fprint(stderr, "dbfs: can't create pipe: %r\n");
raise "fail:pipe";
}
navops := chan of ref Navop;
spawn navigator(navops);
(tchan, srv) := Styxserver.new(fds[0], Navigator.new(navops), big Qdir);
fds[0] = nil;
pidc := chan of int;
spawn serveloop(tchan, srv, pidc, navops);
<-pidc;
if(sys->mount(fds[1], nil, mountpt, flags, nil) < 0) {
sys->fprint(stderr, "dbfs: mount failed: %r\n");
raise "fail:mount";
}
}
rf(f: string): string
{
fd := sys->open(f, Sys->OREAD);
if(fd == nil)
return nil;
b := array[Sys->NAMEMAX] of byte;
n := sys->read(fd, b, len b);
if(n < 0)
return nil;
return string b[0:n];
}
dbread(db: ref Database): (ref Database, string)
{
db.file.seek(big 0, Sys->SEEKSTART);
rl: list of ref Record;
n := 0;
for(;;){
(r, err) := getrec(db);
if(err != nil)
return (nil, err); # could press on without it, or make it the `file' contents
if(r == nil)
break;
rl = r :: rl;
n++;
}
db.nextid = n;
db.records = array[n] of ref Record;
for(; rl != nil; rl = tl rl){
r := hd rl;
n--;
r.id = n;
r.x = n;
db.records[n] = r;
}
return (db, nil);
}
#
# a record is (.+\n)*\n
#
getrec(db: ref Database): (ref Record, string)
{
r := ref Record(-1, -1, 0, 0, nil);
data := "";
for(;;){
s := db.file.gets('\n');
if(s == nil){
if(data == nil)
return (nil, nil); # BUG: distinguish i/o error from EOF?
break;
}
if(s[len s - 1] != '\n')
# return (nil, "file missing newline"); # possibly truncated
s += "\n";
if(s == "\n")
break;
data += s;
}
r.data = array of byte data;
return (r, nil);
}
dbsync(db: ref Database): int
{
if(db.dirty){
db.file = bufio->create(db.name, Sys->OWRITE, 8r666);
if(db.file == nil)
return -1;
for(i := 0; i < len db.records; i++){
r := db.records[i];
if(r != nil && r.data != nil){
if(db.file.write(r.data, len r.data) != len r.data)
return -1;
db.file.putc('\n');
}
}
if(db.file.flush())
return -1;
db.file = nil;
db.dirty = 0;
}
return 0;
}
dbprint(db: ref Database)
{
stdout := sys->fildes(1);
for(i := 0; i < len db.records; i++){
db.records[i].print(stdout);
sys->print("\n");
}
}
Database.findrec(db: self ref Database, id: int): ref Record
{
for(i:=0; i<len db.records; i++)
if((r := db.records[i]) != nil && r.id == id)
return r;
return nil;
}
Record.new(fields: array of byte): ref Record
{
n := len database.records;
r := ref Record(n, n, 0, 0, fields);
a := array[n+1] of ref Record;
if(n)
a[0:] = database.records[0:];
a[n] = r;
database.records = a;
database.vers++;
return r;
}
Record.print(r: self ref Record, fd: ref Sys->FD)
{
if(r.data != nil)
sys->write(fd, r.data, len r.data);
}
Record.qid(r: self ref Record): Sys->Qid
{
return Sys->Qid(QPATH(r.x, Qdata), r.vers, Sys->QTFILE);
}
serveloop(tchan: chan of ref Tmsg, srv: ref Styxserver, pidc: chan of int, navops: chan of ref Navop)
{
pidc <-= sys->pctl(Sys->FORKNS|Sys->NEWFD, 1::2::srv.fd.fd::nil);
Serve:
while((gm := <-tchan) != nil){
pick m := gm {
Readerror =>
sys->fprint(stderr, "dbfs: fatal read error: %s\n", m.error);
break Serve;
Open =>
c := srv.getfid(m.fid);
if(c == nil || TYPE(c.path) != Qnew){
srv.open(m); # default action
break;
}
if(c.uname != user) {
srv.reply(ref Rmsg.Error(m.tag, Eperm));
break;
}
mode := styxservers->openmode(m.mode);
if(mode < 0) {
srv.reply(ref Rmsg.Error(m.tag, Ebadarg));
break;
}
# generate new file, change Fid's qid to match
r := Record.new(array[0] of byte);
qid := r.qid();
c.open(mode, qid);
srv.reply(ref Rmsg.Open(m.tag, qid, srv.iounit()));
Read =>
(c, err) := srv.canread(m);
if(c == nil){
srv.reply(ref Rmsg.Error(m.tag, err));
break;
}
if(c.qtype & Sys->QTDIR){
srv.read(m); # does readdir
break;
}
r := database.records[FILENO(c.path)];
if(r == nil)
srv.reply(ref Rmsg.Error(m.tag, Eremoved));
else
srv.reply(styxservers->readbytes(m, r.data));
Write =>
(c, merr) := srv.canwrite(m);
if(c == nil){
srv.reply(ref Rmsg.Error(m.tag, merr));
break;
}
(value, err) := data2rec(m.data);
if(err != nil){
srv.reply(ref Rmsg.Error(m.tag, err));
break;
}
fno := FILENO(c.path);
r := database.records[fno];
if(r == nil){
srv.reply(ref Rmsg.Error(m.tag, Eremoved));
break;
}
r.data = value;
r.vers++;
database.dirty++;
if(dbsync(database) == 0)
srv.reply(ref Rmsg.Write(m.tag, len m.data));
else
srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
Clunk =>
# a transaction-oriented dbfs could delay updating the record until clunk
srv.clunk(m);
Remove =>
c := srv.getfid(m.fid);
if(c == nil || c.qtype & Sys->QTDIR || TYPE(c.path) != Qdata){
# let it diagnose all the errors
srv.remove(m);
break;
}
r := database.records[FILENO(c.path)];
if(r != nil)
r.data = nil;
database.dirty++;
srv.delfid(c);
if(dbsync(database) == 0)
srv.reply(ref Rmsg.Remove(m.tag));
else
srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
Wstat =>
srv.default(gm); # TO DO?
* =>
srv.default(gm);
}
}
navops <-= nil; # shut down navigator
}
dirslot(n: int): int
{
for(i := 0; i < len database.records; i++){
r := database.records[i];
if(r != nil && r.data != nil){
if(n == 0)
return i;
n--;
}
}
return -1;
}
dir(qid: Sys->Qid, name: string, length: big, uid: string, perm: int): ref Sys->Dir
{
d := ref sys->zerodir;
d.qid = qid;
if(qid.qtype & Sys->QTDIR)
perm |= Sys->DMDIR;
d.mode = perm;
d.name = name;
d.uid = uid;
d.gid = uid;
d.length = length;
return d;
}
dirgen(p: big): (ref Sys->Dir, string)
{
case TYPE(p) {
Qdir =>
return (dir(Qid(QPATH(0, Qdir),database.vers,Sys->QTDIR), "/", big 0, user, 8r700), nil);
Qnew =>
return (dir(Qid(QPATH(0, Qnew),0,Sys->QTFILE), "new", big 0, user, 8r600), nil);
* =>
n := FILENO(p);
if(n < 0 || n >= len database.records)
return (nil, nil);
r := database.records[n];
if(r == nil || r.data == nil)
return (nil, Enotfound);
return (dir(r.qid(), sys->sprint("%d", r.id), big len r.data, user, 8r600), nil);
}
}
navigator(navops: chan of ref Navop)
{
while((m := <-navops) != nil){
pick n := m {
Stat =>
n.reply <-= dirgen(n.path);
Walk =>
if(int n.path != Qdir){
n.reply <-= (nil, "not a directory");
break;
}
case n.name {
".." =>
; # nop
"new" =>
n.path = QPATH(0, Qnew);
* =>
if(len n.name < 1 || !(n.name[0]>='0' && n.name[0]<='9')){ # weak test for now
n.reply <-= (nil, Enotfound);
continue;
}
r := database.findrec(int n.name);
if(r == nil){
n.reply <-= (nil, Enotfound);
continue;
}
n.path = QPATH(r.x, Qdata);
}
n.reply <-= dirgen(n.path);
Readdir =>
if(int m.path != Qdir){
n.reply <-= (nil, "not a directory");
break;
}
i := n.offset;
if(i == 0)
n.reply <-= dirgen(QPATH(0,Qnew));
for(; --n.count >= 0 && (j := dirslot(i)) >= 0; i++)
n.reply <-= dirgen(QPATH(j,Qdata)); # n² but the file will be small
n.reply <-= (nil, nil);
}
}
}
QPATH(w, q: int): big
{
return big ((w<<8)|q);
}
TYPE(path: big): int
{
return int path & 16rFF;
}
FILENO(path: big) : int
{
return (int path >> 8) & 16rFFFFFF;
}
#
# a record is (.+\n)*, without final empty line
#
data2rec(data: array of byte): (array of byte, string)
{
s: string;
for(b := data; len b > 0;){
(b, s) = getline(b);
if(s == nil || s[len s - 1] != '\n' || s == "\n")
return (nil, "partial or malformed record"); # possibly truncated
}
return (data, nil);
}
getline(b: array of byte): (array of byte, string)
{
n := len b;
for(i := 0; i < n; i++){
(ch, l, nil) := sys->byte2char(b, i);
i += l;
if(l == 0 || ch == '\n')
break;
}
return (b[i:], string b[0:i]);
}