parsav  Check-in [af5ed65b68]

Overview
Comment:further iteration on media
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: af5ed65b68e96856432eb7477e8852b2a716281a993d114188c5d6695c014dd2
User & Date: lexi on 2021-01-07 09:39:29
Other Links: manifest | tags
Context
2021-01-07
20:39
media uploads work now, some types can be viewed check-in: 93aea04a05 user: lexi tags: trunk
09:39
further iteration on media check-in: af5ed65b68 user: lexi tags: trunk
07:35
tentative beginnings of upload + media management system check-in: f4c6e72a22 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [5d4cebec04] to [fc1c52be55].

  1587   1587   			for i=0,res.sz do
  1588   1588   				var id = res:int(uint64,i,0)
  1589   1589   				var idbuf: int8[lib.math.shorthand.maxlen]
  1590   1590   				var idlen = lib.math.shorthand.gen(id, &idbuf[0])
  1591   1591   				var desc = res:_string(i,2)
  1592   1592   				var folder = res:_string(i,3)
  1593   1593   				var mime = res:_string(i,4)
  1594         -				var url = lib.str.acc{}:init(48):lpush('/media/a/'):push(&idbuf[0],idlen):finalize() defer url:free()
  1595   1594   				m.ptr[i] = [ lib.str.encapsulate(lib.store.artifact, {
  1596   1595   					desc =  {`desc.ptr, `desc.ct + 1};
  1597   1596   					folder = {`folder.ptr, `folder.ct + 1};
  1598   1597   					mime = {`mime.ptr, `mime.ct + 1};
  1599         -					url = {`url.ptr, `url.ct + 1};
         1598  +					url = {`&idbuf[0], `idlen + 1};
  1600   1599   				}) ]
  1601   1600   				m(i).ptr.rid = id
  1602   1601   				m(i).ptr.owner = uid
  1603   1602   			end
  1604   1603   			return m
  1605   1604   		else return [lib.mem.lstptr(lib.store.artifact)].null() end
  1606   1605   	end];
         1606  +
         1607  +	artifact_load = [terra(
         1608  +		src: &lib.store.source,
         1609  +		rid: uint64
         1610  +	): {binblob, pstring}
         1611  +		var r = queries.artifact_load.exec(src,rid)
         1612  +		if r.sz == 0 then return binblob.null(), pstring.null() end
         1613  +
         1614  +		var mime = r:String(0,1)
         1615  +		var mbin = r:bin(0,0)
         1616  +		var bin = lib.mem.heapa(uint8,mbin.ct)
         1617  +		lib.mem.cpy(bin.ptr, mbin.ptr, bin.ct)
         1618  +
         1619  +		r:free()
         1620  +		return bin, mime
         1621  +	end];
  1607   1622   
  1608   1623   	post_attach_ctl = [terra(
  1609   1624   		src: &lib.store.source,
  1610   1625   		post: uint64,
  1611   1626   		artifact: uint64,
  1612   1627   		detach: bool
  1613   1628   	): {}

Modified math.t from [1d1d177061] to [60110bc615].

   224    224   	var sz: intptr = 0
   225    225   	for i = 0, f.ct do
   226    226   		if f(i) == @',' then goto skip end
   227    227   		if f(i) >= 0x30 and f(i) <= 0x39 then
   228    228   			sz = sz * 10
   229    229   			sz = sz + f(i) - 0x30
   230    230   		else
   231         -			if i+1 == f.ct or f(i) == 0 then return sz, true end
   232         -			if i+2 == f.ct or f(i+1) == 0 then
          231  +			if i+0 == f.ct or f(i) == 0 then return sz, true end
          232  +			if i+1 == f.ct or f(i+1) == 0 then
   233    233   				if f(i) == @'b' then return sz/8, true end -- bits
   234    234   			else
   235    235   				var s: intptr = 0
   236         -				if i+3 == f.ct or f(i+2) == 0 then 
          236  +				if i+2 == f.ct or f(i+2) == 0 then 
   237    237   					s = i + 1
   238         -				elseif (i+4 == f.ct or f(i+3) == 0) and f(i+1) == @'i' then
          238  +				elseif (i+3 == f.ct or f(i+3) == 0) and f(i+1) == @'i' then
   239    239   				-- grudgingly tolerate ~mebibits~ and its ilk, without
   240    240   				-- affecting the result in any way
   241    241   					s = i + 2
   242    242   				else return 0, false end
   243    243   
   244    244   				if f(s) == @'b' then sz = sz/8 -- bits
   245    245   				elseif f(s) ~= @'B' then return 0, false end -- wth
   246    246   			end
   247    247   			var c = f(i)
   248         -			if c >= @'A' and c <= @'Z' then c = c - 0x20 end
          248  +			if c >= @'A' and c <= @'Z' then c = c + 0x20 end
   249    249   			switch c do -- normal char literal syntax doesn't work here, leads to llvm error (!!)
   250    250   				case [uint8]([string.byte('k')]) then return sz * [1024ULL ^ 1], true end
   251    251   				case [uint8]([string.byte('m')]) then return sz * [1024ULL ^ 2], true end
   252    252   				case [uint8]([string.byte('g')]) then return sz * [1024ULL ^ 3], true end
   253    253   				case [uint8]([string.byte('t')]) then return sz * [1024ULL ^ 4], true end
   254         -				case [uint8]([string.byte('e')]) then return sz * [1024ULL ^ 5], true end
   255         -				case [uint8]([string.byte('y')]) then return sz * [1024ULL ^ 6], true end
   256         -				else return sz, true
          254  +				case [uint8]([string.byte('p')]) then return sz * [1024ULL ^ 5], true end
          255  +				case [uint8]([string.byte('e')]) then return sz * [1024ULL ^ 6], true end
          256  +				case [uint8]([string.byte('z')]) then return sz * [1024ULL ^ 7], true end
          257  +				case [uint8]([string.byte('y')]) then return sz * [1024ULL ^ 8], true end
          258  +				else return 0, false
   257    259   			end
   258    260   		end
   259    261   	::skip::end
   260    262   	return sz, true
   261    263   end
   262    264   
   263    265   return m

Modified render/media-gallery.t from [79b3557b2e] to [7a98efa9ff].

     1      1   -- vim: ft=terra
     2      2   local pstr = lib.str.t 
     3      3   local P = lib.str.plit
     4      4   local terra cs(s: rawstring)
     5      5   	return pstr { ptr = s, ct = lib.str.sz(s) }
     6      6   end
     7      7   
     8         -local show_all,show_new,show_files,show_vid,show_img=1,2,3,4,5
            8  +local show_all,show_new,show_unfiled,show_files,show_vid,show_img=1,2,3,4,5,6
     9      9   
    10     10   local terra 
    11     11   render_media_gallery(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), uid: uint64, acc: &lib.str.acc)
    12     12    -- note that when calling this function, path must be adjusted so that path(0)
    13     13    -- eq "media"
    14     14   	var owner = false
    15     15   	if co.aid ~= 0 and co.who.id == uid then owner = true end
    16     16   	var ou = co.srv:actor_fetch_uid(uid)
    17     17   	if not ou then goto e404 end
           18  +
           19  +	var mode: uint8 = show_new
           20  +	var folder: pstr
           21  +	if path.ct == 2 then
           22  +		if path(1):cmp(lib.str.lit'unfiled') then
           23  +			mode=show_unfiled
           24  +		elseif path(1):cmp(lib.str.lit'all') then
           25  +			mode=show_all
           26  +		else goto e404 end
           27  +	elseif path.ct == 3 and path(1):cmp(lib.str.lit'kind') then
           28  +	end
           29  +
           30  +	if mode == show_new then
           31  +		folder = lib.str.plit''
           32  +	elseif mode == show_all then
           33  +		folder = pstr.null()
           34  +	elseif mode == show_unfiled then
           35  +		folder = lib.str.plit'' -- TODO
           36  +	-- else get folder from query str
           37  +	end
    18     38   
    19     39   	var view = data.view.media_gallery {
    20     40   		menu = pstr{'',0};
    21     41   		folders = pstr{'',0};
    22     42   		directory = pstr{'',0};
    23     43   		images = pstr{'',0};
           44  +		pfx = pstr{'',0};
    24     45   	}
           46  +	if not owner then
           47  +		var pa: lib.str.acc pa:init(32)
           48  +		pa:lpush('/')
           49  +		if ou(0).origin ~= 0 then pa:lpush('@') end
           50  +		pa:push(ou(0).xid,0)
           51  +		view.pfx = pa:finalize()
           52  +	end
    25     53   
    26     54   	if owner then
    27     55   		view.menu = P'<a class="pos" href="/media/upload">upload</a><hr>'
    28     56   	end
    29         -	var mode: uint8 = show_new
    30         -	var folder: pstr
    31         -	if mode == show_new then
    32         -		folder = lib.str.plit''
    33         -	elseif mode == show_all then
    34         -		folder = pstr.null()
    35         -	-- else get folder from query str
    36         -	end
    37     57   
    38     58   	var md = co.srv:artifact_enum_uid(uid, folder)
    39     59   	var gallery: lib.str.acc gallery:init(256)
    40     60   	var files: lib.str.acc files:init(256) 
    41     61   	for i=0,md.ct do
    42     62   		if lib.str.ncmp(md(i)(0).mime, 'image/', 6) == 0 then
    43         -			gallery:lpush('<a class="thumb" href="')
    44         -			if not owner then
    45         -				gallery:lpush('/')
    46         -				if ou(0).origin ~= 0 then gallery:lpush('@') end
    47         -				gallery:push(ou(0).xid,0):lpush('/')
    48         -			end
    49         -			gallery:push(md(i)(0).url,0)
    50         -				:lpush('"><img src="') :push(md(i)(0).url,0)
    51         -				:lpush('/raw"><div class="caption">') :push(md(i)(0).desc,0)
           63  +			gallery:lpush('<a class="thumb" href="'):ppush(view.pfx):lpush('/media/a/')
           64  +				:push(md(i)(0).url,0):lpush('"><img src="/file/'):push(md(i)(0).url,0)
           65  +				:lpush('"><div class="caption">'):push(md(i)(0).desc,0)
    52     66   				:lpush('</div></a>')
    53     67   		else
    54         -			files:lpush('<a class="file" href="')
    55         -			if not owner then
    56         -				gallery:lpush('/')
    57         -				if ou(0).origin ~= 0 then gallery:lpush('@') end
    58         -				gallery:push(ou(0).xid,0):lpush('/')
    59         -			end
    60         -			files:push(md(i)(0).url,0)
    61         -				:lpush('"><span class="label">'):push(md(i)(0).desc,0)
           68  +			files:lpush('<a class="file" href="'):ppush(view.pfx):lpush('/media/a/')
           69  +				:push(md(i)(0).url,0):lpush('"><span class="label">'):push(md(i)(0).desc,0)
    62     70   				:lpush('</span> <span class="mime">'):push(md(i)(0).mime,0)
    63     71   				:lpush('</span></a>')
    64     72   		end
    65     73   		md(i):free()
    66     74   	end
    67     75   
    68     76   	view.images = gallery:finalize()
    69     77   	view.directory = files:finalize()
    70     78   
    71     79   	if acc ~= nil then
    72     80   		view:append(acc)
    73     81   	else
           82  +	lib.dbg('emitting page')
    74     83   		var pg = view:tostr() defer pg:free()
           84  +	lib.dbg('compiled page')
    75     85   		co:stdpage([lib.srv.convo.page] {
    76     86   			title = P'media';
    77     87   			class = P'media manager';
    78     88   			cache = false;
    79     89   			body = pg;
    80     90   		})
           91  +	lib.dbg('sent page')
    81     92   	end
    82     93   
    83     94   	view.images:free()
    84     95   	view.directory:free()
           96  +	if not owner then view.pfx:free() end
    85     97   	if md:ref() then md:free() end
    86     98   	do return end
    87     99   
    88    100   	::e404:: co:complain(404,'media not found','no such media exists on this server')
    89    101   end
    90    102   
    91    103   return render_media_gallery

Modified route.t from [af3d41e739] to [8605ea50bd].

   404    404   end
   405    405   
   406    406   terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t)
   407    407   	if meth == method.post then
   408    408   		goto badop
   409    409   	end
   410    410   
   411         -	if path.ct == 1 or (path.ct >= 3 and path(1):cmp(lib.str.lit'a')) then
   412         -		if meth == method.post then goto badop end
   413         -		lib.render.media_gallery(co,path,co.who.id,nil)
   414         -	elseif path.ct == 2 then
   415         -		if path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then
   416         -			if meth == method.get then
   417         -				var view = data.view.media_upload {
   418         -					folders = ''
   419         -				}
   420         -				var pg = view:tostr() defer pg:free()
   421         -				co:stdpage([lib.srv.convo.page] {
   422         -					title = lib.str.plit'media :: upload';
   423         -					class = lib.str.plit'media upload';
   424         -					cache = false; body = pg;
   425         -				})
   426         -			elseif meth == method.post_file then
   427         -				var desc = pstring.null()
   428         -				var folder = pstring.null()
   429         -				var mime = pstring.null()
   430         -				var name = pstring.null()
   431         -				var body = binblob.null()
   432         -				for i=0, co.uploads.sz do var up = co.uploads.storage.ptr + i
   433         -					if up.body.ct > 0 then
   434         -						if up.field:cmp(lib.str.plit'desc') then
   435         -							desc = up.body
   436         -						elseif up.field:cmp(lib.str.plit'folder') then
   437         -							folder = up.body
   438         -						elseif up.field:cmp(lib.str.plit'file') then
   439         -							mime = up.ctype
   440         -							body = binblob {ptr = [&uint8](up.body.ptr), ct = up.body.ct}
   441         -							name = up.filename
   442         -						end
          411  +	if path.ct == 2 and path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then
          412  +		if meth == method.get then
          413  +			var view = data.view.media_upload {
          414  +				folders = ''
          415  +			}
          416  +			var pg = view:tostr() defer pg:free()
          417  +			co:stdpage([lib.srv.convo.page] {
          418  +				title = lib.str.plit'media :: upload';
          419  +				class = lib.str.plit'media upload';
          420  +				cache = false; body = pg;
          421  +			})
          422  +		elseif meth == method.post_file then
          423  +			var desc = pstring.null()
          424  +			var folder = pstring.null()
          425  +			var mime = pstring.null()
          426  +			var name = pstring.null()
          427  +			var body = binblob.null()
          428  +			for i=0, co.uploads.sz do var up = co.uploads.storage.ptr + i
          429  +				if up.body.ct > 0 then
          430  +					if up.field:cmp(lib.str.plit'desc') then
          431  +						desc = up.body
          432  +					elseif up.field:cmp(lib.str.plit'folder') then
          433  +						folder = up.body
          434  +					elseif up.field:cmp(lib.str.plit'file') then
          435  +						mime = up.ctype
          436  +						body = binblob {ptr = [&uint8](up.body.ptr), ct = up.body.ct}
          437  +						name = up.filename
   443    438   					end
   444    439   				end
   445         -				if not body then goto badop end
   446         -				if body.ct > co.srv.cfg.maxupsz then
   447         -					co:complain(403, 'file too long', "the file you have attempted to upload exceeds the maximum length permitted by this server's upload policy. if it is an image or video, try compressing it at a lower quality setting or resolution")
   448         -					return
   449         -				end
   450         -				var id = co.srv:artifact_instantiate(body,mime)
   451         -				if id == 0 then
   452         -					co:complain(500,'upload failed','artifact rejected. either the server is running out of space or this file is banned from the server')
   453         -					return
   454         -				end
   455         -				co.srv:artifact_expropriate(co.who.id,id,desc,folder)
          440  +			end
          441  +			if not body then goto badop end
          442  +			if body.ct > co.srv.cfg.maxupsz then
          443  +				co:complain(403, 'file too long', "the file you have attempted to upload exceeds the maximum length permitted by this server's upload policy. if it is an image or video, try compressing it at a lower quality setting or resolution")
          444  +				return
          445  +			end
          446  +			var id = co.srv:artifact_instantiate(body,mime)
          447  +			if id == 0 then
          448  +				co:complain(500,'upload failed','artifact rejected. either the server is running out of space or this file is banned from the server')
          449  +				return
          450  +			end
          451  +			co.srv:artifact_expropriate(co.who.id,id,desc,folder)
          452  +
          453  +			var idbuf: int8[lib.math.shorthand.maxlen]
          454  +			var idlen = lib.math.shorthand.gen(id,&idbuf[0])
   456    455   
   457         -				var idbuf: int8[lib.math.shorthand.maxlen]
   458         -				var idlen = lib.math.shorthand.gen(id,&idbuf[0])
   459         -
   460         -				var url = lib.str.acc{}:compose('/media/a/',pstring{&idbuf[0],idlen}):finalize()
   461         -				co:reroute(url.ptr)
   462         -				url:free()
   463         -			else goto badop end
   464         -		end
   465         -	else goto e404 end
          456  +			var url = lib.str.acc{}:compose('/media/a/',pstring{&idbuf[0],idlen}):finalize()
          457  +			co:reroute(url.ptr)
          458  +			url:free()
          459  +		else goto badop end
          460  +	else
          461  +		if meth == method.post then goto badop end
          462  +		lib.render.media_gallery(co,path,co.who.id,nil)
          463  +	end
   466    464   	do return end
   467    465   
   468    466   	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
   469    467   	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded by this user') return end
   470    468   end
   471    469   
   472    470   do local branches = quote end
................................................................................
   505    503   end
   506    504   
   507    505   
   508    506   terra http.local_avatar(co: &lib.srv.convo, handle: lib.mem.ptr(int8))
   509    507   	-- TODO retrieve user avatars
   510    508   	co:reroute('/s/default-avatar.webp')
   511    509   end
          510  +
          511  +terra http.file_serve_raw(co: &lib.srv.convo, id: lib.mem.ptr(int8))
          512  +	var id, idok = lib.math.shorthand.parse(id.ptr, id.ct)
          513  +	if not idok then goto e404 end
          514  +	var data, mime = co.srv:artifact_load(id)
          515  +	if not data then goto e404 end
          516  +	do defer data:free() defer mime:free()
          517  +		lib.net.mg_printf(co.con, 'HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n\r\n', mime.ct, mime.ptr, data.ct + 2)
          518  +		lib.net.mg_send(co.con, data.ptr, data.ct)
          519  +		lib.net.mg_send(co.con, '\r\n', 2)
          520  +	return end
          521  +
          522  +	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end
          523  +end
   512    524   
   513    525   -- entry points
   514    526   terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
   515    527   	lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
   516    528   	co.navbar = lib.render.nav(co)
   517    529   	-- some routes are non-hierarchical, and can be resolved with a simple strcmp
   518    530   	-- we run through those first before giving up and parsing the URI
................................................................................
   526    538   	elseif uri.ptr[1] == @'@' then
   527    539   		http.actor_profile_xid(co, uri, meth)
   528    540   	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
   529    541   		if not meth_get(meth) then goto wrongmeth end
   530    542   		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
   531    543   	elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
   532    544   		http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})
          545  +	elseif lib.str.ncmp('/file/', uri.ptr, 6) == 0 then
          546  +		http.file_serve_raw(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 6, ct = uri.ct - 6})
   533    547   	elseif uri:cmp(lib.str.plit '/notices') then
   534    548   		if co.aid == 0 then co:reroute('/login') return end
   535    549   		http.user_notices(co,meth)
   536    550   	elseif uri:cmp(lib.str.plit '/compose') then
   537    551   		if co.aid == 0 then co:reroute('/login') return end
   538    552   		http.post_compose(co,meth)
   539    553   	elseif uri:cmp(lib.str.plit '/login') then
................................................................................
   551    565   		if path.ct > 1 and path(0):cmp(lib.str.lit('user')) then
   552    566   			http.actor_profile_uid(co, path, meth)
   553    567   		elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then
   554    568   			http.tweet_page(co, path, meth)
   555    569   		elseif path(0):cmp(lib.str.lit('tl')) then
   556    570   			http.timeline(co, path)
   557    571   		elseif path(0):cmp(lib.str.lit('media')) then
          572  +			if co.aid == 0 then goto unauth end
   558    573   			http.media_manager(co, path, meth)
   559    574   		elseif path(0):cmp(lib.str.lit('doc')) then
   560    575   			if not meth_get(meth) then goto wrongmeth end
   561    576   			http.documentation(co, path)
   562    577   		elseif path(0):cmp(lib.str.lit('conf')) then
   563    578   			if co.aid == 0 then goto unauth end
   564    579   			http.configure(co,path,meth)

Modified srv.t from [a1d0408148] to [31acbbce83].

   526    526   				end
   527    527   				-- check for a content-type header, and see if it's a multipart/
   528    528   				-- form-data encoded POST request so we can handle file uploads
   529    529   				co.uploads.sz = 0 co.uploads.run = 0
   530    530   				if co.method == [lib.http.method.post] then
   531    531   					var ctt = lib.http.findheader(msg, 'Content-Type')
   532    532   					if ctt ~= nil then
   533         -						lib.dbg('found content type', {ctt.ptr,ctt.ct})
   534    533   						if lib.str.ncmp(ctt.ptr,'multipart/form-data;',20) == 0 then
   535    534   							var p = lib.str.ffw(ctt.ptr + 20,ctt.ct-20)
   536    535   							if lib.str.ncmp(p,'boundary=',9) ~= 0 then
   537    536   								co:complain(400,'bad request','unrecognized content-type')
   538    537   								goto fail
   539    538   							end
   540    539   							var boundary = pstring {ptr=p+9,ct=ctt.ct - ((p - ctt.ptr) + 9)}
   541         -							lib.dbg('got boundary ',{boundary.ptr,boundary.ct})
   542    540   							co.method = lib.http.method.post_file
   543    541   							co.uploads:init(8)
   544    542   
   545    543   							var bsr = (lib.str.acc{}):compose('\r\n--',boundary,'\r\n'):finalize()
   546    544   
   547    545   							var upmap = lib.str.splitmap(co.body,bsr,8)
   548    546   							-- first entry may not be preceded by header-break
................................................................................
   561    559   									lsent.ct = halt.ptr - lsent.ptr
   562    560   								end
   563    561   								lsr:free() end
   564    562   
   565    563   							for i=0,upmap.ct do
   566    564   								var hdrbrk = lib.str.find(upmap(i), lib.str.plit'\r\n\r\n')
   567    565   								if hdrbrk:ref() then
   568         -									lib.dbg('got new entry')
   569    566   									var hdrtxt = pstring {upmap(i).ptr,upmap(i).ct - hdrbrk.ct}
   570    567   									var hdrs = lib.str.splitmap(hdrtxt, '\r\n',6)
   571    568   									var ctt = pstring.null()
   572    569   									var ctd = pstring.null()
   573    570   									for j=0, hdrs.ct do
   574    571   										var brk = lib.str.find(hdrs(j),lib.str.plit':')
   575    572   										if brk:ref() then
................................................................................
   581    578   												ctd = val
   582    579   											end
   583    580   										end
   584    581   									end
   585    582   									if ctd:ref() then
   586    583   										var ctdvals = lib.str.splitmap(ctd, ';', 4) defer ctdvals:free()
   587    584   										if ctdvals(0):cmp(lib.str.plit'form-data') and ctdvals.ct > 1 then
   588         -											lib.dbg('found form data')
   589    585   											var fld = pstring.null()
   590    586   											var file = pstring.null()
   591    587   											for j=1, ctdvals.ct do var v = ctdvals(j):ffw()
   592    588   												var x = lib.str.find(v,lib.str.plit'=')
   593    589   												if x:ref() then
   594    590   													var key = pstring{v.ptr, v.ct - x.ct}
   595    591   													var val = pstring{x.ptr + 1, x.ct - 1}
................................................................................
   776    772   			end
   777    773   		end
   778    774   	end
   779    775   
   780    776   	return 0
   781    777   end
   782    778   
   783         ---9twh8y94i5c1qqr7hxu20fyd
   784    779   terra cfgcache.methods.load :: {&cfgcache} -> {}
   785    780   terra cfgcache:init(o: &srv)
   786    781   	self.overlord = o
   787    782   	self:load()
   788    783   end
   789    784   
   790    785   terra srv:setup(befile: rawstring)

Modified static/live.js from [45ade869ea] to [76c21b64a1].

   117    117   			if (ert != null) { lede = post; post = ert; }
   118    118   			let purl = posturl(post);
   119    119   			let url = null;
   120    120   			if (lede == null) {url = purl;} else {
   121    121   				url = lede.querySelector('a[href].del').
   122    122   					attributes.getNamedItem('href').value;
   123    123   			}
   124         -			console.log('post',post,'lede',lede,url);
   125    124   
   126    125   			if (last == null) { newmap.first = url; } else {
   127    126   				newmap.map.get(last).next = url;
   128    127   			}
   129    128   			newmap.map.set(url, {me: post, go: purl, prev: last, next: null})
   130    129   			last = url;
   131    130   			if (window._liveTweetMap &&

Modified static/style.scss from [7f55f7d5d1] to [f5bd822625].

  1018   1018   		background: tone(-55%,-0.5);
  1019   1019   		border: 1px solid tone(-60%);
  1020   1020   		padding: 0.2in;
  1021   1021   		display: flex;
  1022   1022   		flex-wrap: wrap;
  1023   1023   	}
  1024   1024   	.gallery {
  1025         -		grid-row: 1/2; grid-column: 2/3;
  1026         -		margin-left: 0.1in;
         1025  +		grid-row: 2/3; grid-column: 1/3;
         1026  +		margin-top: 0.1in;
  1027   1027   		flex-flow: row;
         1028  +		padding: 0 0.1in;
  1028   1029   		> a[href].thumb {
         1030  +			background: linear-gradient(to top, tone(10%,-0.8), tone(10%,-0.94) 30%, transparent);
         1031  +			border-radius: 4px;
         1032  +			border-bottom: 1px solid tone(15%, -0.5);
  1029   1033   			display: block;
  1030   1034   			width: 1.5in;
  1031   1035   			padding: 0.1in;
  1032   1036   			height: max-content;
         1037  +			margin: 0.1in;
  1033   1038   			> img {
  1034   1039   				width: 1.5in; height: 1.5in;
         1040  +				object-fit: contain;
         1041  +				object-position: center;
         1042  +				outline: none;
  1035   1043   			}
  1036   1044   			> .caption {
  1037   1045   				text-align: center;
  1038   1046   				font-size: 80%;
         1047  +				text-shadow: 1px 1px 0 black;
  1039   1048   			}
  1040   1049   		}
  1041   1050   	}
  1042   1051   	.dir {
  1043         -		grid-row: 2/3; grid-column: 1/3;
  1044         -		margin-top: 0.1in;
         1052  +		grid-row: 1/2; grid-column: 2/3;
         1053  +		margin-left: 0.1in;
  1045   1054   		flex-flow: column;
  1046   1055   		flex-grow: 1;
  1047   1056   		> a[href].file {
  1048   1057   			padding: 0.1in 0.15in;
  1049   1058   			text-decoration: none;
  1050   1059   			height: max-content;
  1051   1060   			background-image: url(/s/file.webp); //TODO different icons for different mime types

Modified view/media-gallery.tpl from [d752c55f41] to [18862037ee].

     1      1   <menu>@menu
     2         -	<a href="/media">new uploads</a>
     3         -	<a href="/media/unfiled">unfiled</a>
            2  +	<a href="@pfx/media">new uploads</a>
            3  +	<a href="@pfx/media/unfiled">unfiled</a>
     4      4   	<hr>
     5      5   	@folders
     6         -	<a href="/media/all">all uploads</a>
     7         -	<a href="/media/kind/img">all images</a>
     8         -	<a href="/media/kind/vid">all videos</a>
     9         -	<a href="/media/kind/txt">all text files</a>
    10         -	<a href="/media/king/misc">all others</a>
            6  +	<a href="@pfx/media/all">all uploads</a>
            7  +	<a href="@pfx/media/kind/img">all images</a>
            8  +	<a href="@pfx/media/kind/vid">all videos</a>
            9  +	<a href="@pfx/media/kind/txt">all text files</a>
           10  +	<a href="@pfx/media/kind/misc">all others</a>
    11     11   </menu>
    12     12   
    13     13   <div class="dir">
    14     14   	@directory
    15     15   </div>
    16     16   
    17     17   <div class="gallery">
    18     18   	@images
    19     19   </div>