parsav  Check-in [f4c6e72a22]

Overview
Comment:tentative beginnings of upload + media management system
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: f4c6e72a22221e80abe7e4088cf97cb9b1d6aca0cd3a9593343d12aaa6e4ec2f
User & Date: lexi on 2021-01-07 07:35:14
Other Links: manifest | tags
Context
2021-01-07
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
2021-01-06
22:31
unfuck last commit check-in: 8d8ab01573 user: lexi tags: trunk
Changes

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

   477    477   			limit case when $4::bigint = 0 then null
   478    478   					   else $4::bigint end
   479    479   			offset $5::bigint
   480    480   		]];
   481    481   	};
   482    482   
   483    483   	artifact_instantiate = {
   484         -		params = {binblob, binblob, pstring}, sql = [[
   485         -			insert into parsav_artifacts (content,hash,mime) values (
   486         -				$1::bytea, $2::bytea, $3::text
          484  +		params = {binblob, binblob, pstring, int64}, sql = [[
          485  +			insert into parsav_artifacts (content,hash,mime,birth) values (
          486  +				$1::bytea, $2::bytea, $3::text,$4::bigint
   487    487   			) on conflict do nothing returning id
   488    488   		]];
   489    489   	};
   490    490   	artifact_expropriate = {
   491         -		params = {uint64, uint64, pstring}, cmd = true, sql = [[
   492         -			insert into parsav_artifact_claims (uid,rid,description,folder) values (
   493         -				$1::bigint, $2::bigint, $3::text, 'new'
          491  +		params = {uint64, uint64, pstring, pstring, int64}, cmd = true, sql = [[
          492  +			insert into parsav_artifact_claims (uid,rid,description,folder,birth) values (
          493  +				$1::bigint, $2::bigint, $3::text, $4::text, $5::bigint
   494    494   			) on conflict do nothing
   495    495   		]];
   496    496   	};
   497    497   	artifact_quicksearch = {
   498    498   		params = {binblob}, sql = [[
   499    499   			select id, (content is null) from parsav_artifacts where hash = $1::bytea
   500    500   				limit 1
................................................................................
   529    529   	-- "ERROR:  cannot insert multiple commands into a prepared
   530    530   	--  statement" are you fucking shitting me with this shit
   531    531   		params = {uint64}, sql = [[
   532    532   			delete from parsav_artifact_claims where
   533    533   				rid = $1::bigint
   534    534   			returning uid, description, birth, folder;
   535    535   		]];
          536  +	};
          537  +	artifact_enum_uid = {
          538  +		params = {uint64, pstring}, sql = [[
          539  +			select (pg_temp.parsavpg_translate_artifact(a)).*
          540  +			from parsav_artifact_claims as a where uid = $1::bigint and
          541  +				($2::text is null or
          542  +				 ($2::text = '' and folder is null) or
          543  +				 $2::text = folder)
          544  +			order by birth desc
          545  +		]];
          546  +	};
          547  +	artifact_fetch = {
          548  +		params = {uint64, uint64}, sql = [[
          549  +			select (pg_temp.parsavpg_translate_artifact(a)).*
          550  +			from parsav_artifact_claims as a where uid = $1::bigint and rid = $2::bigint
          551  +		]];
          552  +	};
          553  +	artifact_load = {
          554  +		params = {uint64}, sql = [[
          555  +			select content, mime from parsav_artifacts where id = $1::bigint
          556  +		]];
   536    557   	};
   537    558   	post_attach_ctl_ins = {
   538    559   		params = {uint64, uint64}, cmd=true, sql = [[
   539    560   			update parsav_posts set
   540    561   				artifacts = artifacts || $2::bigint
   541    562   			where id = $1::bigint and not
   542    563   				artifacts @> array[$2::bigint] -- prevent duplication
................................................................................
  1530   1551   			if ban then
  1531   1552   				lib.report('user attempted to instantiate forsaken artifact')
  1532   1553   				return 0
  1533   1554   			end
  1534   1555   			var oldid = srec:int(uint64,0,0)
  1535   1556   			return oldid
  1536   1557   		else -- not in db, insert
  1537         -			var nrec = queries.artifact_instantiate.exec(src, artifact, hashb, mime)
         1558  +			var nrec = queries.artifact_instantiate.exec(src, artifact, hashb, mime, lib.osclock.time(nil))
  1538   1559   			if nrec.sz == 0 then
  1539   1560   				lib.warn('failed to instantiate artifact -- are you running out of storage?')
  1540   1561   				return 0
  1541   1562   			else defer nrec:free()
  1542   1563   				var newid = nrec:int(uint64,0,0)
  1543   1564   				return newid
  1544   1565   			end
  1545   1566   		end
  1546   1567   	end];
         1568  +
         1569  +	artifact_expropriate = [terra(
         1570  +		src: &lib.store.source,
         1571  +		uid: uint64,
         1572  +		artifact: uint64,
         1573  +		desc: pstring,
         1574  +		folder: pstring
         1575  +	): {}
         1576  +		queries.artifact_expropriate.exec(src,uid,artifact,desc,folder, lib.osclock.time(nil))
         1577  +	end];
         1578  +
         1579  +	artifact_enum_uid = [terra(
         1580  +		src: &lib.store.source,
         1581  +		uid: uint64,
         1582  +		folder: pstring
         1583  +	)
         1584  +		var res = queries.artifact_enum_uid.exec(src,uid,folder)
         1585  +		if res.sz > 0 then
         1586  +			var m = lib.mem.heapa([lib.mem.ptr(lib.store.artifact)], res.sz)
         1587  +			for i=0,res.sz do
         1588  +				var id = res:int(uint64,i,0)
         1589  +				var idbuf: int8[lib.math.shorthand.maxlen]
         1590  +				var idlen = lib.math.shorthand.gen(id, &idbuf[0])
         1591  +				var desc = res:_string(i,2)
         1592  +				var folder = res:_string(i,3)
         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  +				m.ptr[i] = [ lib.str.encapsulate(lib.store.artifact, {
         1596  +					desc =  {`desc.ptr, `desc.ct + 1};
         1597  +					folder = {`folder.ptr, `folder.ct + 1};
         1598  +					mime = {`mime.ptr, `mime.ct + 1};
         1599  +					url = {`url.ptr, `url.ct + 1};
         1600  +				}) ]
         1601  +				m(i).ptr.rid = id
         1602  +				m(i).ptr.owner = uid
         1603  +			end
         1604  +			return m
         1605  +		else return [lib.mem.lstptr(lib.store.artifact)].null() end
         1606  +	end];
  1547   1607   
  1548   1608   	post_attach_ctl = [terra(
  1549   1609   		src: &lib.store.source,
  1550   1610   		post: uint64,
  1551   1611   		artifact: uint64,
  1552   1612   		detach: bool
  1553   1613   	): {}

Modified backend/schema/pgsql-views.sql from [d567971842] to [ca832d14af].

    15     15   	select p.id as post,
    16     16   		coalesce((select counts.ct from counts where counts.subject = p.id
    17     17   			and counts.kind = 'like'),0)::integer as likes,
    18     18   		coalesce((select counts.ct from counts where counts.subject = p.id
    19     19   			and counts.kind = 'rt'  ),0)::integer as rts
    20     20   	from parsav_posts as p
    21     21   );
           22  +
           23  +create type pg_temp.parsavpg_intern_artifact as (
           24  +	rid		bigint,
           25  +	owner	bigint,
           26  +	"desc"	text,
           27  +	folder	text,
           28  +	mime	text
           29  +);
           30  +
           31  +create or replace function
           32  +pg_temp.parsavpg_translate_artifact(parsav_artifact_claims)
           33  +returns pg_temp.parsavpg_intern_artifact as $$
           34  +	select ($1).rid, ($1).uid, ($1).description, ($1).folder, a.mime
           35  +	from parsav_artifacts a where
           36  +		a.id = ($1).rid limit 1
           37  +$$ language sql;
    22     38   
    23     39   create type pg_temp.parsavpg_intern_notice as (
    24     40   	kind	smallint,
    25     41   	"when"	bigint,
    26     42   	who		bigint,
    27     43   	what	bigint,
    28     44   	reply	bigint,

Modified config.lua from [eb323f3b45] to [6cff716428].

    57     57   		{'bell.svg', 'image/svg+xml'};
    58     58   		{'heart.webp', 'image/webp'};
    59     59   		{'retweet.webp', 'image/webp'};
    60     60   		{'padlock.svg', 'image/svg+xml'};
    61     61   		{'warn.svg', 'image/svg+xml'};
    62     62   		{'query.webp', 'image/webp'};
    63     63   		{'reply.webp', 'image/webp'};
           64  +		{'file.webp', 'image/webp'};
    64     65   		-- keep in mind before you add anything to this list: these are not
    65     66   		-- just files parsav can access, they are files that are *kept in
    66     67   		-- memory* for fast access the entire time parsav is running, and
    67     68   		-- which need to be loaded into memory before the program can even
    68     69   		-- start. it's imperative to keep these as small and few in number
    69     70   		-- as is realistically possible.
    70     71   	};

Modified http.t from [7092b409fb] to [4c9f723184].

     1      1   -- vim: ft=terra
     2      2   local m = {}
     3      3   local util = lib.util
     4      4   
     5         -m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' }
            5  +m.method = lib.enum { 'get', 'post', 'post_file', 'head', 'options', 'put', 'delete' }
     6      6   m.mime = lib.enum {
     7      7   	'html'; -- default
     8      8   	'json';
     9      9   	'mkdown';
    10     10   	'text';
    11     11   	'ansi';
    12     12   	'none';
................................................................................
    18     18   	key: rawstring
    19     19   	value: rawstring
    20     20   }
    21     21   struct m.page {
    22     22   	respcode: uint16
    23     23   	body: lib.mem.ptr(int8)
    24     24   	headers: lib.mem.ptr(m.header)
           25  +}
           26  +struct m.upload {
           27  +	ctype: lib.str.t;
           28  +	filename: lib.str.t;
           29  +	field: lib.str.t;
           30  +	body: lib.str.t;
    25     31   }
    26     32   
    27     33   local resps = {
    28     34   	[200] = 'OK';
    29     35   	[201] = 'Created';
    30     36   	[301] = 'Moved Permanently';
    31     37   	[302] = 'Found';

Modified makefile from [0e9ec3efb8] to [8559ac7b2c].

     1      1   dl = git
     2      2   dbg-flags = $(if $(dbg),-g)
     3      3   
     4         -images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp static/reply.webp
            4  +images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp static/reply.webp static/file.webp
     5      5   #$(addsuffix .webp, $(basename $(wildcard static/*.svg)))
     6      6   styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))
     7      7   
     8      8   parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles)
     9      9   	terra $(dbg-flags) $<
    10     10   parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles)
    11     11   	env parsav_link=no terra $(dbg-flags) $<

Modified parsav.t from [8bdefbaeb6] to [7b59e1e979].

   434    434   	'render:profile';
   435    435   	'render:compose';
   436    436   	'render:tweet';
   437    437   	'render:tweet-page';
   438    438   	'render:user-page';
   439    439   	'render:timeline';
   440    440   	'render:notices';
          441  +
          442  +	'render:media-gallery';
   441    443   
   442    444   	'render:docpage';
   443    445   
   444    446   	'render:conf:profile';
   445    447   	'render:conf:sec';
   446    448   	'render:conf:users';
   447    449   	'render:conf';

Added render/media-gallery.t version [79b3557b2e].

            1  +-- vim: ft=terra
            2  +local pstr = lib.str.t 
            3  +local P = lib.str.plit
            4  +local terra cs(s: rawstring)
            5  +	return pstr { ptr = s, ct = lib.str.sz(s) }
            6  +end
            7  +
            8  +local show_all,show_new,show_files,show_vid,show_img=1,2,3,4,5
            9  +
           10  +local terra 
           11  +render_media_gallery(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), uid: uint64, acc: &lib.str.acc)
           12  + -- note that when calling this function, path must be adjusted so that path(0)
           13  + -- eq "media"
           14  +	var owner = false
           15  +	if co.aid ~= 0 and co.who.id == uid then owner = true end
           16  +	var ou = co.srv:actor_fetch_uid(uid)
           17  +	if not ou then goto e404 end
           18  +
           19  +	var view = data.view.media_gallery {
           20  +		menu = pstr{'',0};
           21  +		folders = pstr{'',0};
           22  +		directory = pstr{'',0};
           23  +		images = pstr{'',0};
           24  +	}
           25  +
           26  +	if owner then
           27  +		view.menu = P'<a class="pos" href="/media/upload">upload</a><hr>'
           28  +	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  +
           38  +	var md = co.srv:artifact_enum_uid(uid, folder)
           39  +	var gallery: lib.str.acc gallery:init(256)
           40  +	var files: lib.str.acc files:init(256) 
           41  +	for i=0,md.ct do
           42  +		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)
           52  +				:lpush('</div></a>')
           53  +		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)
           62  +				:lpush('</span> <span class="mime">'):push(md(i)(0).mime,0)
           63  +				:lpush('</span></a>')
           64  +		end
           65  +		md(i):free()
           66  +	end
           67  +
           68  +	view.images = gallery:finalize()
           69  +	view.directory = files:finalize()
           70  +
           71  +	if acc ~= nil then
           72  +		view:append(acc)
           73  +	else
           74  +		var pg = view:tostr() defer pg:free()
           75  +		co:stdpage([lib.srv.convo.page] {
           76  +			title = P'media';
           77  +			class = P'media manager';
           78  +			cache = false;
           79  +			body = pg;
           80  +		})
           81  +	end
           82  +
           83  +	view.images:free()
           84  +	view.directory:free()
           85  +	if md:ref() then md:free() end
           86  +	do return end
           87  +
           88  +	::e404:: co:complain(404,'media not found','no such media exists on this server')
           89  +end
           90  +
           91  +return render_media_gallery

Modified render/nav.t from [62959bc993] to [5194b2263f].

     3      3   render_nav(co: &lib.srv.convo)
     4      4   	var t: lib.str.acc t:init(64)
     5      5   	if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then
     6      6   		t:lpush(' <a accesskey="t" href="/">timeline</a>')
     7      7   	end
     8      8   	if co.who ~= nil then
     9      9   		t:lpush(' <a accesskey="c" href="/compose">compose</a> <a accesskey="p" href="/'):push(co.who.xid,0)
    10         -		t:lpush('">profile</a> <a accesskey="o" href="/conf">configure</a> <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/logout">log out</a> <a class="bell" href="/notices">notices</a>')
           10  +		t:lpush('">profile</a> <a accesskey="m" href="/media">media</a> <a accesskey="o" href="/conf">configure</a> <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/logout">log out</a> <a class="bell" href="/notices">notices</a>')
    11     11   	else
    12     12   		t:lpush(' <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/login">log in</a>')
    13     13   	end
    14     14   	return t:finalize()
    15     15   end
    16     16   return render_nav

Modified route.t from [e8c2143a0c] to [af3d41e739].

     1      1   -- vim: ft=terra
     2      2   local r = lib.srv.route
     3      3   local method = lib.http.method
     4      4   local pstring = lib.mem.ptr(int8)
     5      5   local rstring = lib.mem.ref(int8)
            6  +local binblob = lib.mem.ptr(uint8)
     6      7   local hpath = lib.mem.ptr(rstring)
     7      8   local http = {}
     8      9   
     9     10   terra meth_get(meth: method.t) return (meth == method.get) or (meth == method.head) end
    10     11   
    11     12   terra http.actor_profile(co: &lib.srv.convo, actor: &lib.store.actor, meth: method.t)
    12     13   	var rel: lib.store.relationship
................................................................................
   395    396   			return
   396    397   		else goto badop end
   397    398   	end
   398    399   
   399    400   	lib.render.notices(co)
   400    401   	do return end
   401    402   
   402         -	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
          403  +	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
          404  +end
          405  +
          406  +terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t)
          407  +	if meth == method.post then
          408  +		goto badop
          409  +	end
          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
          443  +					end
          444  +				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)
          456  +
          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
          466  +	do return end
          467  +
          468  +	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
          469  +	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded by this user') return end
   403    470   end
   404    471   
   405    472   do local branches = quote end
   406    473   	local filename, flen = symbol(&int8), symbol(intptr)
   407    474   	local page = symbol(lib.http.page)
   408    475   	local send = label()
   409    476   	local storage = data.stmap
................................................................................
   483    550   		var path = lib.http.hier(uri) defer path:free()
   484    551   		if path.ct > 1 and path(0):cmp(lib.str.lit('user')) then
   485    552   			http.actor_profile_uid(co, path, meth)
   486    553   		elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then
   487    554   			http.tweet_page(co, path, meth)
   488    555   		elseif path(0):cmp(lib.str.lit('tl')) then
   489    556   			http.timeline(co, path)
          557  +		elseif path(0):cmp(lib.str.lit('media')) then
          558  +			http.media_manager(co, path, meth)
   490    559   		elseif path(0):cmp(lib.str.lit('doc')) then
   491    560   			if not meth_get(meth) then goto wrongmeth end
   492    561   			http.documentation(co, path)
   493    562   		elseif path(0):cmp(lib.str.lit('conf')) then
   494    563   			if co.aid == 0 then goto unauth end
   495    564   			http.configure(co,path,meth)
   496    565   		else goto notfound end

Modified srv.t from [d4dcecb4e5] to [a1d0408148].

   134    134   	aid: uint64 -- 0 if logged out
   135    135   	aid_issue: lib.store.timepoint
   136    136   	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
   137    137   	peer: lib.store.inet
   138    138   	reqtype: lib.http.mime.t -- negotiated content type
   139    139   	method: lib.http.method.t
   140    140   	live_last: lib.store.timepoint
          141  +	uploads: lib.mem.vec(lib.http.upload)
          142  +	body: lib.str.t
   141    143   -- cache
   142    144   	ui_hue: uint16
   143    145   	navbar: lib.mem.ptr(int8)
   144    146   	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
   145    147   -- private
   146    148   	varbuf: lib.mem.ptr(int8)
   147    149   	vbofs: &int8
................................................................................
   328    330   	else return nil, 0 end
   329    331   end
   330    332   terra convo:pgetv(name: rawstring)
   331    333   	var s,l = self:getv(name)
   332    334   	return pstring { ptr = s, ct = l }
   333    335   end
   334    336   
   335         -local urimatch = macro(function(uri, ptn)
   336         -	return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1)
   337         -end)
   338         -
   339    337   local route = {} -- these are defined in route.t, as they need access to renderers
   340    338   terra route.dispatch_http ::  {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}
   341    339   
   342    340   local mimetypes = {
   343    341   	{'html', 'text/html'};
   344    342   	{'json', 'application/json'};
   345    343   	{'mkdown', 'text/markdown'};
................................................................................
   391    389   					reqtype = lib.http.mime.none;
   392    390   					peer = peer, live_last = 0;
   393    391   				} co.varbuf.ptr = nil
   394    392   				  co.navbar.ptr = nil
   395    393   				  co.actorcache.top = 0
   396    394   				  co.actorcache.cur = 0
   397    395   				  co.ui_hue = server.cfg.ui_hue
          396  +				  co.body.ptr = msg.body.ptr co.body.ct = msg.body.len
   398    397   
   399    398   				-- first, check for an accept header. if it's there, we need to
   400    399   				-- iterate over the values and pick the highest-priority one
   401    400   				do var acc = lib.http.findheader(msg, 'Accept')
   402    401   					-- TODO handle q-value
   403    402   					if acc ~= nil and acc.ptr ~= nil then
   404    403   						var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr }
................................................................................
   511    510   					end
   512    511   					uri.ct = msg.uri.len
   513    512   				else uri.ct = urideclen end
   514    513   				lib.dbg('routing URI ', {uri.ptr, uri.ct})
   515    514   				
   516    515   				if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then
   517    516   					co.method = [lib.http.method.get]
   518         -					route.dispatch_http(&co, uri, [lib.http.method.get])
   519    517   				elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then
   520         -					co.method = [lib.http.method.get]
   521         -					route.dispatch_http(&co, uri, [lib.http.method.post])
          518  +					co.method = [lib.http.method.post]
   522    519   				elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then
   523    520   					co.method = [lib.http.method.head]
   524         -					route.dispatch_http(&co, uri, [lib.http.method.head])
   525    521   				elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then
   526    522   					co.method = [lib.http.method.options]
   527         -					route.dispatch_http(&co, uri, [lib.http.method.options])
   528    523   				else
   529    524   					co:complain(400,'unknown method','you have submitted an invalid http request')
          525  +					goto fail
          526  +				end
          527  +				-- check for a content-type header, and see if it's a multipart/
          528  +				-- form-data encoded POST request so we can handle file uploads
          529  +				co.uploads.sz = 0 co.uploads.run = 0
          530  +				if co.method == [lib.http.method.post] then
          531  +					var ctt = lib.http.findheader(msg, 'Content-Type')
          532  +					if ctt ~= nil then
          533  +						lib.dbg('found content type', {ctt.ptr,ctt.ct})
          534  +						if lib.str.ncmp(ctt.ptr,'multipart/form-data;',20) == 0 then
          535  +							var p = lib.str.ffw(ctt.ptr + 20,ctt.ct-20)
          536  +							if lib.str.ncmp(p,'boundary=',9) ~= 0 then
          537  +								co:complain(400,'bad request','unrecognized content-type')
          538  +								goto fail
          539  +							end
          540  +							var boundary = pstring {ptr=p+9,ct=ctt.ct - ((p - ctt.ptr) + 9)}
          541  +							lib.dbg('got boundary ',{boundary.ptr,boundary.ct})
          542  +							co.method = lib.http.method.post_file
          543  +							co.uploads:init(8)
          544  +
          545  +							var bsr = (lib.str.acc{}):compose('\r\n--',boundary,'\r\n'):finalize()
          546  +
          547  +							var upmap = lib.str.splitmap(co.body,bsr,8)
          548  +							-- first entry may not be preceded by header-break
          549  +							if lib.str.find(upmap(0), pstring {
          550  +								ptr = bsr.ptr + 2, ct = bsr.ct - 2
          551  +							}):ref() then
          552  +								upmap(0).ptr = upmap(0).ptr + (bsr.ct - 2)
          553  +								upmap(0).ct = upmap(0).ct - (bsr.ct - 2)
          554  +							end
          555  +
          556  +							-- last entry is weird
          557  +							do var lsr = (lib.str.acc{}):compose('\r\n--',boundary,'--\r\n'):finalize()
          558  +								var lsent = upmap.ptr + (upmap.ct - 1)
          559  +								var halt = lib.str.find(@lsent, lsr)
          560  +								if halt:ref() then
          561  +									lsent.ct = halt.ptr - lsent.ptr
          562  +								end
          563  +								lsr:free() end
          564  +
          565  +							for i=0,upmap.ct do
          566  +								var hdrbrk = lib.str.find(upmap(i), lib.str.plit'\r\n\r\n')
          567  +								if hdrbrk:ref() then
          568  +									lib.dbg('got new entry')
          569  +									var hdrtxt = pstring {upmap(i).ptr,upmap(i).ct - hdrbrk.ct}
          570  +									var hdrs = lib.str.splitmap(hdrtxt, '\r\n',6)
          571  +									var ctt = pstring.null()
          572  +									var ctd = pstring.null()
          573  +									for j=0, hdrs.ct do
          574  +										var brk = lib.str.find(hdrs(j),lib.str.plit':')
          575  +										if brk:ref() then
          576  +											var hdr = pstring{hdrs(j).ptr,hdrs(j).ct - brk.ct}
          577  +											var val = pstring{brk.ptr+1, brk.ct-1}:ffw()
          578  +											if hdr:cmp(lib.str.plit'Content-Type') then
          579  +												ctt = val
          580  +											elseif hdr:cmp(lib.str.plit'Content-Disposition') then
          581  +												ctd = val
          582  +											end
          583  +										end
          584  +									end
          585  +									if ctd:ref() then
          586  +										var ctdvals = lib.str.splitmap(ctd, ';', 4) defer ctdvals:free()
          587  +										if ctdvals(0):cmp(lib.str.plit'form-data') and ctdvals.ct > 1 then
          588  +											lib.dbg('found form data')
          589  +											var fld = pstring.null()
          590  +											var file = pstring.null()
          591  +											for j=1, ctdvals.ct do var v = ctdvals(j):ffw()
          592  +												var x = lib.str.find(v,lib.str.plit'=')
          593  +												if x:ref() then
          594  +													var key = pstring{v.ptr, v.ct - x.ct}
          595  +													var val = pstring{x.ptr + 1, x.ct - 1}
          596  +													var decval, ofs, sp = lib.str.toknext(val,@';',true)
          597  +													if key:cmp(lib.str.plit'name') then
          598  +														fld = decval
          599  +													elseif key:cmp(lib.str.plit'filename') then
          600  +														file = decval
          601  +													else decval:free() end
          602  +												end
          603  +											end
          604  +											if fld:ref() then
          605  +												var nextup = co.uploads:new()
          606  +												if ctt:ref() then
          607  +													nextup.ctype = ctt
          608  +												else
          609  +													nextup.ctype = pstring.null()
          610  +												end
          611  +												nextup.body = pstring {
          612  +													ptr = hdrbrk.ptr + 4;
          613  +													ct = hdrbrk.ct - 4;
          614  +												}
          615  +												nextup.ctype = ctt
          616  +												nextup.field = fld
          617  +												nextup.filename = file
          618  +											end
          619  +										end
          620  +									end
          621  +								end
          622  +							end
          623  +							bsr:free()
          624  +							upmap:free()
          625  +						end
          626  +					end
          627  +				end
          628  +
          629  +				route.dispatch_http(&co, uri, co.method)
          630  +				if co.uploads.run > 0 then
          631  +					for i=0,co.uploads.sz do
          632  +						co.uploads(i).filename:free()
          633  +						co.uploads(i).field:free()
          634  +					end
          635  +					co.uploads:free()
   530    636   				end
   531    637   
          638  +				::fail::
   532    639   				if co.aid ~= 0 then lib.mem.heapf(co.who) end
   533    640   				if co.varbuf.ptr ~= nil then co.varbuf:free() end
   534    641   				if co.navbar.ptr ~= nil then co.navbar:free() end
   535    642   				co.actorcache:free()
   536    643   			end
   537    644   		end
   538    645   	end;

Added static/file.svg version [c89d070bea].

            1  +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
            2  +<!-- Created with Inkscape (http://www.inkscape.org/) -->
            3  +
            4  +<svg
            5  +   xmlns:dc="http://purl.org/dc/elements/1.1/"
            6  +   xmlns:cc="http://creativecommons.org/ns#"
            7  +   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
            8  +   xmlns:svg="http://www.w3.org/2000/svg"
            9  +   xmlns="http://www.w3.org/2000/svg"
           10  +   xmlns:xlink="http://www.w3.org/1999/xlink"
           11  +   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
           12  +   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
           13  +   width="20"
           14  +   height="20"
           15  +   viewBox="0 0 5.2916664 5.2916665"
           16  +   version="1.1"
           17  +   id="svg8"
           18  +   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
           19  +   sodipodi:docname="file.svg">
           20  +  <defs
           21  +     id="defs2">
           22  +    <linearGradient
           23  +       inkscape:collect="always"
           24  +       id="linearGradient935">
           25  +      <stop
           26  +         style="stop-color:#39104b;stop-opacity:1;"
           27  +         offset="0"
           28  +         id="stop931" />
           29  +      <stop
           30  +         style="stop-color:#39104b;stop-opacity:0;"
           31  +         offset="1"
           32  +         id="stop933" />
           33  +    </linearGradient>
           34  +    <linearGradient
           35  +       id="linearGradient923"
           36  +       inkscape:collect="always">
           37  +      <stop
           38  +         id="stop919"
           39  +         offset="0"
           40  +         style="stop-color:#f0cfff;stop-opacity:1" />
           41  +      <stop
           42  +         id="stop921"
           43  +         offset="1"
           44  +         style="stop-color:#eabcff;stop-opacity:0;" />
           45  +    </linearGradient>
           46  +    <linearGradient
           47  +       inkscape:collect="always"
           48  +       id="linearGradient904">
           49  +      <stop
           50  +         style="stop-color:#df9aff;stop-opacity:1;"
           51  +         offset="0"
           52  +         id="stop900" />
           53  +      <stop
           54  +         style="stop-color:#df9aff;stop-opacity:0;"
           55  +         offset="1"
           56  +         id="stop902" />
           57  +    </linearGradient>
           58  +    <linearGradient
           59  +       inkscape:collect="always"
           60  +       id="linearGradient896">
           61  +      <stop
           62  +         style="stop-color:#eabcff;stop-opacity:1;"
           63  +         offset="0"
           64  +         id="stop892" />
           65  +      <stop
           66  +         style="stop-color:#eabcff;stop-opacity:0;"
           67  +         offset="1"
           68  +         id="stop894" />
           69  +    </linearGradient>
           70  +    <linearGradient
           71  +       inkscape:collect="always"
           72  +       id="linearGradient954">
           73  +      <stop
           74  +         style="stop-color:#ffffff;stop-opacity:1;"
           75  +         offset="0"
           76  +         id="stop950" />
           77  +      <stop
           78  +         style="stop-color:#ffffff;stop-opacity:0;"
           79  +         offset="1"
           80  +         id="stop952" />
           81  +    </linearGradient>
           82  +    <linearGradient
           83  +       inkscape:collect="always"
           84  +       id="linearGradient938">
           85  +      <stop
           86  +         style="stop-color:#d9fff6;stop-opacity:1;"
           87  +         offset="0"
           88  +         id="stop934" />
           89  +      <stop
           90  +         style="stop-color:#d9fff6;stop-opacity:0;"
           91  +         offset="1"
           92  +         id="stop936" />
           93  +    </linearGradient>
           94  +    <linearGradient
           95  +       inkscape:collect="always"
           96  +       id="linearGradient1403">
           97  +      <stop
           98  +         style="stop-color:#ccaaff;stop-opacity:1;"
           99  +         offset="0"
          100  +         id="stop1399" />
          101  +      <stop
          102  +         style="stop-color:#ccaaff;stop-opacity:0;"
          103  +         offset="1"
          104  +         id="stop1401" />
          105  +    </linearGradient>
          106  +    <linearGradient
          107  +       id="linearGradient1395"
          108  +       inkscape:collect="always">
          109  +      <stop
          110  +         id="stop1391"
          111  +         offset="0"
          112  +         style="stop-color:#ff1616;stop-opacity:1" />
          113  +      <stop
          114  +         id="stop1393"
          115  +         offset="1"
          116  +         style="stop-color:#ff1d1d;stop-opacity:0" />
          117  +    </linearGradient>
          118  +    <linearGradient
          119  +       inkscape:collect="always"
          120  +       id="linearGradient1383">
          121  +      <stop
          122  +         style="stop-color:#980000;stop-opacity:1;"
          123  +         offset="0"
          124  +         id="stop1379" />
          125  +      <stop
          126  +         style="stop-color:#980000;stop-opacity:0;"
          127  +         offset="1"
          128  +         id="stop1381" />
          129  +    </linearGradient>
          130  +    <linearGradient
          131  +       inkscape:collect="always"
          132  +       id="linearGradient832">
          133  +      <stop
          134  +         style="stop-color:#ffcfcf;stop-opacity:1;"
          135  +         offset="0"
          136  +         id="stop828" />
          137  +      <stop
          138  +         style="stop-color:#ffcfcf;stop-opacity:0;"
          139  +         offset="1"
          140  +         id="stop830" />
          141  +    </linearGradient>
          142  +    <radialGradient
          143  +       inkscape:collect="always"
          144  +       xlink:href="#linearGradient832"
          145  +       id="radialGradient834"
          146  +       cx="3.2286437"
          147  +       cy="286.62921"
          148  +       fx="3.2286437"
          149  +       fy="286.62921"
          150  +       r="1.0866126"
          151  +       gradientTransform="matrix(1.8608797,0.8147617,-0.38242057,0.87343168,106.71446,33.692223)"
          152  +       gradientUnits="userSpaceOnUse" />
          153  +    <radialGradient
          154  +       inkscape:collect="always"
          155  +       xlink:href="#linearGradient1383"
          156  +       id="radialGradient1385"
          157  +       cx="4.1787109"
          158  +       cy="286.89261"
          159  +       fx="4.1787109"
          160  +       fy="286.89261"
          161  +       r="1.2260786"
          162  +       gradientTransform="matrix(1.7016464,0,0,1.6348586,-2.9319775,-182.10895)"
          163  +       gradientUnits="userSpaceOnUse" />
          164  +    <radialGradient
          165  +       inkscape:collect="always"
          166  +       xlink:href="#linearGradient1395"
          167  +       id="radialGradient1389"
          168  +       gradientUnits="userSpaceOnUse"
          169  +       gradientTransform="matrix(0.66230313,-1.6430738,1.0154487,0.40931507,-290.06307,177.39489)"
          170  +       cx="4.02069"
          171  +       cy="287.79269"
          172  +       fx="4.02069"
          173  +       fy="287.79269"
          174  +       r="1.0866126" />
          175  +    <linearGradient
          176  +       inkscape:collect="always"
          177  +       xlink:href="#linearGradient1403"
          178  +       id="linearGradient1405"
          179  +       x1="8.3939333"
          180  +       y1="288.1091"
          181  +       x2="7.0158253"
          182  +       y2="287.32819"
          183  +       gradientUnits="userSpaceOnUse" />
          184  +    <linearGradient
          185  +       inkscape:collect="always"
          186  +       xlink:href="#linearGradient938"
          187  +       id="linearGradient940"
          188  +       x1="7.609839"
          189  +       y1="288.73215"
          190  +       x2="7.609839"
          191  +       y2="283.78305"
          192  +       gradientUnits="userSpaceOnUse" />
          193  +    <linearGradient
          194  +       inkscape:collect="always"
          195  +       xlink:href="#linearGradient954"
          196  +       id="linearGradient956"
          197  +       x1="3.0150654"
          198  +       y1="285.94464"
          199  +       x2="3.0150654"
          200  +       y2="282.40109"
          201  +       gradientUnits="userSpaceOnUse" />
          202  +    <linearGradient
          203  +       inkscape:collect="always"
          204  +       xlink:href="#linearGradient954"
          205  +       id="linearGradient1138"
          206  +       gradientUnits="userSpaceOnUse"
          207  +       x1="3.0150654"
          208  +       y1="285.94464"
          209  +       x2="3.0150654"
          210  +       y2="284.62277" />
          211  +    <linearGradient
          212  +       inkscape:collect="always"
          213  +       xlink:href="#linearGradient896"
          214  +       id="linearGradient898"
          215  +       x1="2.6224887"
          216  +       y1="20"
          217  +       x2="2.6224887"
          218  +       y2="-0.44642866"
          219  +       gradientUnits="userSpaceOnUse"
          220  +       gradientTransform="matrix(0.26458333,0,0,0.26458333,2.6134662,283.36966)" />
          221  +    <linearGradient
          222  +       inkscape:collect="always"
          223  +       xlink:href="#linearGradient904"
          224  +       id="linearGradient906"
          225  +       x1="5.1028705"
          226  +       y1="285.45639"
          227  +       x2="6.1422977"
          228  +       y2="284.41696"
          229  +       gradientUnits="userSpaceOnUse"
          230  +       gradientTransform="translate(1.4605056e-7,1.403324e-5)" />
          231  +    <linearGradient
          232  +       inkscape:collect="always"
          233  +       xlink:href="#linearGradient923"
          234  +       id="linearGradient915"
          235  +       gradientUnits="userSpaceOnUse"
          236  +       x1="2.6224887"
          237  +       y1="-7.0215807"
          238  +       x2="2.6224887"
          239  +       y2="19.346249" />
          240  +    <linearGradient
          241  +       inkscape:collect="always"
          242  +       xlink:href="#linearGradient904"
          243  +       id="linearGradient927"
          244  +       gradientUnits="userSpaceOnUse"
          245  +       gradientTransform="matrix(1.1002873,0,0,1.1002873,-0.68825328,-28.478577)"
          246  +       x1="5.2755661"
          247  +       y1="285.28369"
          248  +       x2="5.7849226"
          249  +       y2="284.77432" />
          250  +    <radialGradient
          251  +       inkscape:collect="always"
          252  +       xlink:href="#linearGradient935"
          253  +       id="radialGradient939"
          254  +       cx="6.3029079"
          255  +       cy="284.65445"
          256  +       fx="6.3029079"
          257  +       fy="284.65445"
          258  +       r="1.6035197"
          259  +       gradientTransform="matrix(1.3125186,0,0,1.1401099,-1.643629,-40.275795)"
          260  +       gradientUnits="userSpaceOnUse" />
          261  +  </defs>
          262  +  <sodipodi:namedview
          263  +     id="base"
          264  +     pagecolor="#181818"
          265  +     bordercolor="#666666"
          266  +     borderopacity="1.0"
          267  +     inkscape:pageopacity="0"
          268  +     inkscape:pageshadow="2"
          269  +     inkscape:zoom="2.8"
          270  +     inkscape:cx="-104.22073"
          271  +     inkscape:cy="-48.222179"
          272  +     inkscape:document-units="mm"
          273  +     inkscape:current-layer="layer1"
          274  +     showgrid="false"
          275  +     units="px"
          276  +     inkscape:window-width="1920"
          277  +     inkscape:window-height="1042"
          278  +     inkscape:window-x="0"
          279  +     inkscape:window-y="38"
          280  +     inkscape:window-maximized="0"
          281  +     showguides="false"
          282  +     fit-margin-top="0"
          283  +     fit-margin-left="0"
          284  +     fit-margin-right="0"
          285  +     fit-margin-bottom="0" />
          286  +  <metadata
          287  +     id="metadata5">
          288  +    <rdf:RDF>
          289  +      <cc:Work
          290  +         rdf:about="">
          291  +        <dc:format>image/svg+xml</dc:format>
          292  +        <dc:type
          293  +           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
          294  +        <dc:title></dc:title>
          295  +      </cc:Work>
          296  +    </rdf:RDF>
          297  +  </metadata>
          298  +  <g
          299  +     inkscape:label="Layer 1"
          300  +     inkscape:groupmode="layer"
          301  +     id="layer1"
          302  +     transform="translate(-2.6134661,-283.36966)">
          303  +    <path
          304  +       sodipodi:type="inkscape:offset"
          305  +       inkscape:radius="1.2412"
          306  +       inkscape:original="M 5.9453125 2.2695312 C 4.8334737 2.2695312 3.9394531 3.1635518 3.9394531 4.2753906 L 3.9394531 15.724609 C 3.9394531 16.836448 4.8334737 17.730469 5.9453125 17.730469 L 14.054688 17.730469 C 15.166526 17.730469 16.060547 16.836448 16.060547 15.724609 L 16.060547 7.6113281 L 10.71875 2.2695312 L 5.9453125 2.2695312 z "
          307  +       style="opacity:0.223;vector-effect:none;fill:url(#linearGradient915);fill-opacity:1;stroke:none;stroke-width:0.62362206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          308  +       id="path913"
          309  +       d="m 5.9453125,1.0292969 c -1.777198,0 -3.2460937,1.4688957 -3.2460937,3.2460937 V 15.724609 c -1e-7,1.777199 1.4688954,3.246094 3.2460937,3.246094 h 8.1093755 c 1.777198,0 3.246093,-1.468895 3.246093,-3.246094 V 7.6113281 A 1.2413241,1.2413241 0 0 0 16.9375,6.734375 L 11.595703,1.3925781 A 1.2413241,1.2413241 0 0 0 10.71875,1.0292969 Z"
          310  +       transform="matrix(0.26458333,0,0,0.26458333,2.6134662,283.36966)" />
          311  +    <path
          312  +       style="opacity:1;vector-effect:none;fill:url(#linearGradient898);fill-opacity:1;stroke:none;stroke-width:0.16500001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          313  +       d="m 4.1864967,283.97014 c -0.294174,0 -0.5307169,0.23654 -0.5307169,0.53072 v 3.02927 c 0,0.29417 0.2365429,0.53072 0.5307169,0.53072 h 2.1456056 c 0.2941738,0 0.5307169,-0.23655 0.5307169,-0.53072 v -2.14664 l -1.4133505,-1.41335 z"
          314  +       id="rect882"
          315  +       inkscape:connector-curvature="0" />
          316  +    <path
          317  +       inkscape:connector-curvature="0"
          318  +       id="path929"
          319  +       d="m 4.1864967,283.97014 c -0.294174,0 -0.5307169,0.23654 -0.5307169,0.53072 v 3.02927 c 0,0.29417 0.2365429,0.53072 0.5307169,0.53072 h 2.1456056 c 0.2941738,0 0.5307169,-0.23655 0.5307169,-0.53072 v -2.14664 l -1.4133505,-1.41335 z"
          320  +       style="opacity:0.71;vector-effect:none;fill:url(#radialGradient939);fill-opacity:1;stroke:none;stroke-width:0.16500001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
          321  +    <path
          322  +       inkscape:connector-curvature="0"
          323  +       id="path925"
          324  +       d="m 5.3077278,283.97015 v 0.97114 c 0,0.32367 0.2602653,0.58395 0.583941,0.58395 h 0.9711506 z"
          325  +       style="fill:url(#linearGradient927);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;opacity:0.449" />
          326  +    <path
          327  +       style="fill:url(#linearGradient906);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
          328  +       d="m 5.4494687,283.97014 v 0.88263 c 0,0.29417 0.2365431,0.53072 0.5307169,0.53072 h 0.8826336 z"
          329  +       id="path890"
          330  +       inkscape:connector-curvature="0" />
          331  +  </g>
          332  +</svg>

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

  1002   1002   		}
  1003   1003   		> article.post {
  1004   1004   			margin: 0.1in 0.2in;
  1005   1005   			margin-left: 0.4in;
  1006   1006   		}
  1007   1007   	}
  1008   1008   }
         1009  +
         1010  +.media.manager main, .media.gallery {
         1011  +	display: grid;
         1012  +	grid-template-columns: 2in 1fr;
         1013  +	grid-template-rows: max-content 1fr;
         1014  +	menu {
         1015  +		@extend %navmenu;
         1016  +	}
         1017  +	.gallery, .dir {
         1018  +		background: tone(-55%,-0.5);
         1019  +		border: 1px solid tone(-60%);
         1020  +		padding: 0.2in;
         1021  +		display: flex;
         1022  +		flex-wrap: wrap;
         1023  +	}
         1024  +	.gallery {
         1025  +		grid-row: 1/2; grid-column: 2/3;
         1026  +		margin-left: 0.1in;
         1027  +		flex-flow: row;
         1028  +		> a[href].thumb {
         1029  +			display: block;
         1030  +			width: 1.5in;
         1031  +			padding: 0.1in;
         1032  +			height: max-content;
         1033  +			> img {
         1034  +				width: 1.5in; height: 1.5in;
         1035  +			}
         1036  +			> .caption {
         1037  +				text-align: center;
         1038  +				font-size: 80%;
         1039  +			}
         1040  +		}
         1041  +	}
         1042  +	.dir {
         1043  +		grid-row: 2/3; grid-column: 1/3;
         1044  +		margin-top: 0.1in;
         1045  +		flex-flow: column;
         1046  +		flex-grow: 1;
         1047  +		> a[href].file {
         1048  +			padding: 0.1in 0.15in;
         1049  +			text-decoration: none;
         1050  +			height: max-content;
         1051  +			background-image: url(/s/file.webp); //TODO different icons for different mime types
         1052  +			background-repeat: no-repeat;
         1053  +			background-position: left;
         1054  +			padding-left: 0.4in;
         1055  +			> .label {
         1056  +				text-decoration: underline;
         1057  +			}
         1058  +			> .mime {
         1059  +				font-style: italic;
         1060  +				opacity: 60%;
         1061  +				margin-left: 0.5ex;
         1062  +			}
         1063  +		}
         1064  +	}
         1065  +}
         1066  +
         1067  +.media.upload form {
         1068  +	padding: 0.1in 0.2in;
         1069  +	@extend %box;
         1070  +}

Modified store.t from [54fed43947] to [fdc1c1d9e2].

   226    226   	rtdby: uint64 -- 0 if not rt
   227    227   	rtact: uint64 -- 0 if not rt, id of rt action otherwise
   228    228   	isreply: bool
   229    229   	source: &m.source
   230    230   
   231    231   	-- save :: bool -> {} (defined in acl.t due to dep. hell)
   232    232   }
          233  +
          234  +struct m.artifact {
          235  +	rid: uint64
          236  +	owner: uint64
          237  +	desc: str
          238  +	folder: str
          239  +	mime: str
          240  +	url: str
          241  +}
   233    242   
   234    243   m.user_conf_funcs = function(be,n,ty,rty,rty2)
   235    244   	rty = rty or ty
   236    245   	local gt
   237    246   	if not rty2 -- what the fuck?
   238    247   		then gt = {&m.source, uint64, rawstring} -> rty;
   239    248   		else gt = {&m.source, uint64, rawstring} -> {rty, rty2};
................................................................................
   447    456   			-- artifact: bytea
   448    457   			-- mime:     pstring
   449    458   	artifact_quicksearch: {&m.source, lib.mem.ptr(uint8)} -> {uint64,bool}
   450    459   		-- checks whether a hash is already in the database without uploading
   451    460   		-- the entire file to the database server
   452    461   			-- hash: bytea
   453    462   				--> artifact id (0 if null), suppressed?
   454         -	artifact_expropriate: {&m.source, uint64, uint64, lib.mem.ptr(int8)} -> {}
          463  +	artifact_expropriate: {&m.source, uint64, uint64, lib.str.t, lib.str.t} -> {}
   455    464   		-- claims an existing artifact for the user's own collection
   456    465   			-- uid:         uint64
   457    466   			-- artifact id: uint64
   458    467   			-- description: pstring
          468  +			-- folder:      pstring
          469  +	artifact_claim_alter: {&m.source, uint64, uint64, lib.str.t, lib.str.t} -> {}
          470  +		-- edits an existing claim to an artifact
          471  +			-- ibid
   459    472   	artifact_disclaim: {&m.source, uint64, uint64} -> {}
   460    473   		-- a user disclaims their ownership stake in an artifact, removing it from
   461    474   		-- the database entirely if they were the only owner, and removing their
   462    475   		-- description of it either way
   463    476   			-- uid:         uint64
   464    477   			-- artifact id: uint64
   465    478   	artifact_excise: {&m.source, uint64, bool} -> {}
................................................................................
   466    479   		-- (admin action) forcibly excise an artifact from the database, deleting
   467    480   		-- all links to it and removing it from users' collections. if "blacklist,"
   468    481   		-- the artifact will be banned and attempts to upload it in the future
   469    482   		-- will fail, triggering a report. mainly intended for dealing with spam,
   470    483   		-- IP violations, That Which Shall Not Be Named, and various other infohazards.
   471    484   			-- artifact id: uint64
   472    485   			-- blacklist:   bool
          486  +	artifact_enum_uid: {&m.source, uint64, lib.str.t} -> lib.mem.lstptr(m.artifact)
          487  +		-- produces a list of artifacts claimed by a user, optionally
          488  +		-- restricted by folder (empty string = new only)
          489  +	artifact_fetch: {&m.source, uint64, uint64} -> lib.mem.ptr(m.artifact)
          490  +		-- fetch a user's view of an artifact
          491  +			-- uid: uint64
          492  +			-- rid: uint64
          493  +	artifact_load: {&m.source, uint64} -> {lib.mem.ptr(uint8),lib.str.t}
          494  +		-- load the body of an artifact into memory (also returns mime)
   473    495   
   474    496   	nkvd_report_issue: {&m.source, &m.kompromat} -> {}
   475    497   		-- an incidence of Badthink has been detected. report it immediately
   476    498   		-- to the Supreme Soviet
   477    499   	nkvd_reports_enum: {&m.source, &m.kompromat} -> lib.mem.ptr(m.kompromat)
   478    500   		-- search through the Archives
   479    501   			-- proto: kompromat (null for all records, or a prototype describing the records to return)

Modified str.t from [004fceba8a] to [d98a573fe7].

    17     17   	ndup = terralib.externfunction('strndup',{rawstring, intptr} -> rawstring);
    18     18   	fmt = terralib.externfunction('asprintf',
    19     19   		terralib.types.funcpointer({&rawstring,rawstring},{int},true));
    20     20   	bfmt = terralib.externfunction('sprintf',
    21     21   		terralib.types.funcpointer({rawstring,rawstring},{int},true));
    22     22   	span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring);
    23     23   }
           24  +
           25  +terra m.ffw(str: &int8, maxlen: intptr)
           26  +	if maxlen == 0 then maxlen = m.sz(str) end
           27  +	while maxlen > 0 and @str ~= 0 and
           28  +	      (@str == @' ' or @str == @'\t' or @str == @'\n') do
           29  +		str = str + 1
           30  +		maxlen = maxlen - 1
           31  +	end
           32  +	return str
           33  +end
           34  +
    24     35   
    25     36   do local strptr = (lib.mem.ptr(int8))
    26     37   	local strref = (lib.mem.ref(int8))
    27     38   	local byteptr = (lib.mem.ptr(uint8))
    28     39   	strptr.metamethods.__cast = function(from,to,e)
    29     40   		if from == &int8 then
    30     41   			return `strptr {ptr = e, ct = m.sz(e)}
................................................................................
    51     62   		var sz = lib.math.biggest(self.ct, other.ct)
    52     63   		for i = 0, sz do
    53     64   			if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end
    54     65   			if self.ptr[i] ~= other.ptr[i] then return false end
    55     66   		end
    56     67   		return true
    57     68   	end
    58         -
           69  +	terra strptr:ffw()
           70  +		var newp = m.ffw(self.ptr,self.ct)
           71  +		var newct = self.ct - (newp - self.ptr)
           72  +		return strptr { ptr = newp, ct = newct }
           73  +	end
    59     74   	strptr.methods.cmpl = macro(function(self,other)
    60     75   		return `self:cmp(strptr { ptr = [other:asvalue()], ct = [#(other:asvalue())] })
    61     76   	end)
    62     77   	strref.methods.cmpl = macro(function(self,other)
    63     78   		return `self:cmp(strref { ptr = [other:asvalue()], ct = [#(other:asvalue())] })
    64     79   	end)
    65     80   
................................................................................
   357    372   		for j=0, reject.ct do
   358    373   			if str.ptr[i] == reject.ptr[j] then return i end
   359    374   		end
   360    375   	end
   361    376   	return maxlen
   362    377   end
   363    378   
   364         -terra m.ffw(str: &int8, maxlen: intptr)
   365         -	while maxlen > 0 and @str ~= 0 and
   366         -	      (@str == @' ' or @str == @'\t' or @str == @'\n') do
   367         -		str = str + 1
   368         -		maxlen = maxlen - 1
   369         -	end
   370         -	return str
   371         -end
   372         -
   373    379   terra m.ffw_unsafe(str: &int8)
   374    380   	while  @str ~= 0 and
   375    381   	      (@str == @' ' or @str == @'\t' or @str == @'\n') do
   376    382   		str = str + 1
   377    383   	end
   378    384   	return str
   379    385   end
          386  +
          387  +terra m.find(haystack: pstr, needle: pstr): pstr
          388  +	for i=0,haystack.ct do
          389  +		for j=0, needle.ct do
          390  +			if haystack(i + j) ~= needle(j) then goto nomatch end
          391  +		end
          392  +		do return pstr {
          393  +			ptr = haystack.ptr + i;
          394  +			ct = haystack.ct - i;
          395  +		} end
          396  +	::nomatch::end
          397  +	return pstr.null()
          398  +end
          399  +
          400  +terra m.splitmap(str: pstr, delim: pstr, expect: uint16)
          401  +	var vec: lib.mem.vec(pstr) vec:init(expect)
          402  +	var start = pstr{str.ptr, str.ct}
          403  +	while true do
          404  +		var n = m.find(start, delim)
          405  +		if not n then break end
          406  +		vec:push(pstr {ptr = start.ptr, ct = start.ct - n.ct})
          407  +		n.ptr = n.ptr + delim.ct
          408  +		n.ct = n.ct - delim.ct
          409  +		start = n
          410  +	end
          411  +	vec:push(start)
          412  +	return vec:crush()
          413  +end
          414  +
          415  +terra m.toknext(str: m.t, delim: int8, brkspace: bool): {pstr,intptr,bool}
          416  +	var b: m.acc b:init(48)
          417  +	var mode: int8 = 0
          418  +	var esc = false
          419  +	var spacebroke = false
          420  +	var max = 0
          421  +	for i=0, str.ct do
          422  +		max = i
          423  +		if str(i) == 0         then break
          424  +		elseif esc == true     then b:push(str.ptr + i,1) esc = false
          425  +		elseif str(i) == @'\\' then esc = true
          426  +
          427  +		elseif mode == 0 and str(i) == delim then break
          428  +		elseif mode ~= 2 and str(i) == @'"'  then
          429  +			if mode == 1
          430  +				then mode = 0
          431  +				else mode = 1
          432  +			end
          433  +		elseif mode ~= 1 and str(i) == @"'" then
          434  +			if mode == 2
          435  +				then mode = 0
          436  +				else mode = 2
          437  +			end
          438  +
          439  +		elseif brkspace and mode == 0 and (
          440  +			str(i) == @' ' or str(i) == @'\t' or
          441  +			str(i) == @'\r' or str(i) == @'\n') then
          442  +			spacebroke = true
          443  +			break
          444  +
          445  +		else b:push(str.ptr + i,1) end
          446  +	end
          447  +	if mode ~= 0 then return m.t.null(), 0, false end
          448  +
          449  +	return b:finalize(), max, spacebroke
          450  +end
   380    451   
   381    452   return m

Modified view/load.lua from [1dbf5e584d] to [fbf23a2927].

     6      6   local sources = {
     7      7   	'docskel';
     8      8   	'confirm';
     9      9   	'tweet';
    10     10   	'profile';
    11     11   	'compose';
    12     12   	'notice';
           13  +
           14  +	'media-gallery';
           15  +	'media-upload';
    13     16   
    14     17   	'login-username';
    15     18   	'login-challenge';
    16     19   
    17     20   	'conf';
    18     21   	'conf-profile';
    19     22   	'conf-sec';

Added view/media-gallery.tpl version [d752c55f41].

            1  +<menu>@menu
            2  +	<a href="/media">new uploads</a>
            3  +	<a href="/media/unfiled">unfiled</a>
            4  +	<hr>
            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>
           11  +</menu>
           12  +
           13  +<div class="dir">
           14  +	@directory
           15  +</div>
           16  +
           17  +<div class="gallery">
           18  +	@images
           19  +</div>

Added view/media-upload.tpl version [687485d89c].

            1  +<form method="post" enctype="multipart/form-data">
            2  +	<div class="elem">
            3  +		<label for="file">file</label>
            4  +		<input type="file" name="file" id="file" required>
            5  +	</div>
            6  +	<div class="elem">
            7  +		<label for="desc">description</label>
            8  +		<textarea name="desc" id="desc" placeholder="soviet troops planting the red flag on olympus mons after the battle of tharsis (1969)"></textarea>
            9  +	</div>
           10  +	<div class="elem">
           11  +		<label for="folder">folder</label>
           12  +		<input type="text" name="folder" id="folder" list="folders">
           13  +	</div>
           14  +	<menu class="choice horizontal">
           15  +		<button>upload</button>
           16  +		<a class="button" href="/media">cancel</a>
           17  +	</menu>
           18  +</form>
           19  +
           20  +<datalist id="folders">
           21  +	@folders
           22  +</datalist>

Deleted view/media.tpl version [5a68c18a8e].

     1         -<menu>
     2         -	<a href="/user/@:xid/media">new uploads</a>
     3         -	@folders
     4         -</menu>
     5         -
     6         -<div name="gallery">
     7         -	@images
     8         -</div>
     9         -
    10         -<div name="files">
    11         -	@files
    12         -</div>