Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -479,20 +479,20 @@ offset $5::bigint ]]; }; artifact_instantiate = { - params = {binblob, binblob, pstring}, sql = [[ - insert into parsav_artifacts (content,hash,mime) values ( - $1::bytea, $2::bytea, $3::text + params = {binblob, binblob, pstring, int64}, sql = [[ + insert into parsav_artifacts (content,hash,mime,birth) values ( + $1::bytea, $2::bytea, $3::text,$4::bigint ) on conflict do nothing returning id ]]; }; artifact_expropriate = { - params = {uint64, uint64, pstring}, cmd = true, sql = [[ - insert into parsav_artifact_claims (uid,rid,description,folder) values ( - $1::bigint, $2::bigint, $3::text, 'new' + params = {uint64, uint64, pstring, pstring, int64}, cmd = true, sql = [[ + insert into parsav_artifact_claims (uid,rid,description,folder,birth) values ( + $1::bigint, $2::bigint, $3::text, $4::text, $5::bigint ) on conflict do nothing ]]; }; artifact_quicksearch = { params = {binblob}, sql = [[ @@ -531,10 +531,31 @@ params = {uint64}, sql = [[ delete from parsav_artifact_claims where rid = $1::bigint returning uid, description, birth, folder; ]]; + }; + artifact_enum_uid = { + params = {uint64, pstring}, sql = [[ + select (pg_temp.parsavpg_translate_artifact(a)).* + from parsav_artifact_claims as a where uid = $1::bigint and + ($2::text is null or + ($2::text = '' and folder is null) or + $2::text = folder) + order by birth desc + ]]; + }; + artifact_fetch = { + params = {uint64, uint64}, sql = [[ + select (pg_temp.parsavpg_translate_artifact(a)).* + from parsav_artifact_claims as a where uid = $1::bigint and rid = $2::bigint + ]]; + }; + artifact_load = { + params = {uint64}, sql = [[ + select content, mime from parsav_artifacts where id = $1::bigint + ]]; }; post_attach_ctl_ins = { params = {uint64, uint64}, cmd=true, sql = [[ update parsav_posts set artifacts = artifacts || $2::bigint @@ -1532,20 +1553,59 @@ return 0 end var oldid = srec:int(uint64,0,0) return oldid else -- not in db, insert - var nrec = queries.artifact_instantiate.exec(src, artifact, hashb, mime) + var nrec = queries.artifact_instantiate.exec(src, artifact, hashb, mime, lib.osclock.time(nil)) if nrec.sz == 0 then lib.warn('failed to instantiate artifact -- are you running out of storage?') return 0 else defer nrec:free() var newid = nrec:int(uint64,0,0) return newid end end end]; + + artifact_expropriate = [terra( + src: &lib.store.source, + uid: uint64, + artifact: uint64, + desc: pstring, + folder: pstring + ): {} + queries.artifact_expropriate.exec(src,uid,artifact,desc,folder, lib.osclock.time(nil)) + end]; + + artifact_enum_uid = [terra( + src: &lib.store.source, + uid: uint64, + folder: pstring + ) + var res = queries.artifact_enum_uid.exec(src,uid,folder) + if res.sz > 0 then + var m = lib.mem.heapa([lib.mem.ptr(lib.store.artifact)], res.sz) + for i=0,res.sz do + var id = res:int(uint64,i,0) + var idbuf: int8[lib.math.shorthand.maxlen] + var idlen = lib.math.shorthand.gen(id, &idbuf[0]) + var desc = res:_string(i,2) + var folder = res:_string(i,3) + var mime = res:_string(i,4) + var url = lib.str.acc{}:init(48):lpush('/media/a/'):push(&idbuf[0],idlen):finalize() defer url:free() + m.ptr[i] = [ lib.str.encapsulate(lib.store.artifact, { + desc = {`desc.ptr, `desc.ct + 1}; + folder = {`folder.ptr, `folder.ct + 1}; + mime = {`mime.ptr, `mime.ct + 1}; + url = {`url.ptr, `url.ct + 1}; + }) ] + m(i).ptr.rid = id + m(i).ptr.owner = uid + end + return m + else return [lib.mem.lstptr(lib.store.artifact)].null() end + end]; post_attach_ctl = [terra( src: &lib.store.source, post: uint64, artifact: uint64, Index: backend/schema/pgsql-views.sql ================================================================== --- backend/schema/pgsql-views.sql +++ backend/schema/pgsql-views.sql @@ -17,10 +17,26 @@ and counts.kind = 'like'),0)::integer as likes, coalesce((select counts.ct from counts where counts.subject = p.id and counts.kind = 'rt' ),0)::integer as rts from parsav_posts as p ); + +create type pg_temp.parsavpg_intern_artifact as ( + rid bigint, + owner bigint, + "desc" text, + folder text, + mime text +); + +create or replace function +pg_temp.parsavpg_translate_artifact(parsav_artifact_claims) +returns pg_temp.parsavpg_intern_artifact as $$ + select ($1).rid, ($1).uid, ($1).description, ($1).folder, a.mime + from parsav_artifacts a where + a.id = ($1).rid limit 1 +$$ language sql; create type pg_temp.parsavpg_intern_notice as ( kind smallint, "when" bigint, who bigint, Index: config.lua ================================================================== --- config.lua +++ config.lua @@ -59,10 +59,11 @@ {'retweet.webp', 'image/webp'}; {'padlock.svg', 'image/svg+xml'}; {'warn.svg', 'image/svg+xml'}; {'query.webp', 'image/webp'}; {'reply.webp', 'image/webp'}; + {'file.webp', 'image/webp'}; -- keep in mind before you add anything to this list: these are not -- just files parsav can access, they are files that are *kept in -- memory* for fast access the entire time parsav is running, and -- which need to be loaded into memory before the program can even -- start. it's imperative to keep these as small and few in number Index: http.t ================================================================== --- http.t +++ http.t @@ -1,10 +1,10 @@ -- vim: ft=terra local m = {} local util = lib.util -m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' } +m.method = lib.enum { 'get', 'post', 'post_file', 'head', 'options', 'put', 'delete' } m.mime = lib.enum { 'html'; -- default 'json'; 'mkdown'; 'text'; @@ -20,10 +20,16 @@ } struct m.page { respcode: uint16 body: lib.mem.ptr(int8) headers: lib.mem.ptr(m.header) +} +struct m.upload { + ctype: lib.str.t; + filename: lib.str.t; + field: lib.str.t; + body: lib.str.t; } local resps = { [200] = 'OK'; [201] = 'Created'; Index: makefile ================================================================== --- makefile +++ makefile @@ -1,9 +1,9 @@ dl = git dbg-flags = $(if $(dbg),-g) -images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp static/reply.webp +images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp static/reply.webp static/file.webp #$(addsuffix .webp, $(basename $(wildcard static/*.svg))) styles = $(addsuffix .css, $(basename $(wildcard static/*.scss))) parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles) terra $(dbg-flags) $< Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -436,10 +436,12 @@ 'render:tweet'; 'render:tweet-page'; 'render:user-page'; 'render:timeline'; 'render:notices'; + + 'render:media-gallery'; 'render:docpage'; 'render:conf:profile'; 'render:conf:sec'; ADDED render/media-gallery.t Index: render/media-gallery.t ================================================================== --- render/media-gallery.t +++ render/media-gallery.t @@ -0,0 +1,91 @@ +-- vim: ft=terra +local pstr = lib.str.t +local P = lib.str.plit +local terra cs(s: rawstring) + return pstr { ptr = s, ct = lib.str.sz(s) } +end + +local show_all,show_new,show_files,show_vid,show_img=1,2,3,4,5 + +local terra +render_media_gallery(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), uid: uint64, acc: &lib.str.acc) + -- note that when calling this function, path must be adjusted so that path(0) + -- eq "media" + var owner = false + if co.aid ~= 0 and co.who.id == uid then owner = true end + var ou = co.srv:actor_fetch_uid(uid) + if not ou then goto e404 end + + var view = data.view.media_gallery { + menu = pstr{'',0}; + folders = pstr{'',0}; + directory = pstr{'',0}; + images = pstr{'',0}; + } + + if owner then + view.menu = P'upload
' + end + var mode: uint8 = show_new + var folder: pstr + if mode == show_new then + folder = lib.str.plit'' + elseif mode == show_all then + folder = pstr.null() + -- else get folder from query str + end + + var md = co.srv:artifact_enum_uid(uid, folder) + var gallery: lib.str.acc gallery:init(256) + var files: lib.str.acc files:init(256) + for i=0,md.ct do + if lib.str.ncmp(md(i)(0).mime, 'image/', 6) == 0 then + gallery:lpush('
') :push(md(i)(0).desc,0) + :lpush('
') + else + files:lpush(''):push(md(i)(0).desc,0) + :lpush(' '):push(md(i)(0).mime,0) + :lpush('') + end + md(i):free() + end + + view.images = gallery:finalize() + view.directory = files:finalize() + + if acc ~= nil then + view:append(acc) + else + var pg = view:tostr() defer pg:free() + co:stdpage([lib.srv.convo.page] { + title = P'media'; + class = P'media manager'; + cache = false; + body = pg; + }) + end + + view.images:free() + view.directory:free() + if md:ref() then md:free() end + do return end + + ::e404:: co:complain(404,'media not found','no such media exists on this server') +end + +return render_media_gallery Index: render/nav.t ================================================================== --- render/nav.t +++ render/nav.t @@ -5,12 +5,12 @@ if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then t:lpush(' timeline') end if co.who ~= nil then t:lpush(' compose profile configure docs log out notices') + t:lpush('">profile media configure docs log out notices') else t:lpush(' docs log in') end return t:finalize() end return render_nav Index: route.t ================================================================== --- route.t +++ route.t @@ -1,10 +1,11 @@ -- vim: ft=terra local r = lib.srv.route local method = lib.http.method local pstring = lib.mem.ptr(int8) local rstring = lib.mem.ref(int8) +local binblob = lib.mem.ptr(uint8) local hpath = lib.mem.ptr(rstring) local http = {} terra meth_get(meth: method.t) return (meth == method.get) or (meth == method.head) end @@ -397,11 +398,77 @@ end lib.render.notices(co) do return end - ::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end + ::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end +end + +terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t) + if meth == method.post then + goto badop + end + + if path.ct == 1 or (path.ct >= 3 and path(1):cmp(lib.str.lit'a')) then + if meth == method.post then goto badop end + lib.render.media_gallery(co,path,co.who.id,nil) + elseif path.ct == 2 then + if path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then + if meth == method.get then + var view = data.view.media_upload { + folders = '' + } + var pg = view:tostr() defer pg:free() + co:stdpage([lib.srv.convo.page] { + title = lib.str.plit'media :: upload'; + class = lib.str.plit'media upload'; + cache = false; body = pg; + }) + elseif meth == method.post_file then + var desc = pstring.null() + var folder = pstring.null() + var mime = pstring.null() + var name = pstring.null() + var body = binblob.null() + for i=0, co.uploads.sz do var up = co.uploads.storage.ptr + i + if up.body.ct > 0 then + if up.field:cmp(lib.str.plit'desc') then + desc = up.body + elseif up.field:cmp(lib.str.plit'folder') then + folder = up.body + elseif up.field:cmp(lib.str.plit'file') then + mime = up.ctype + body = binblob {ptr = [&uint8](up.body.ptr), ct = up.body.ct} + name = up.filename + end + end + end + if not body then goto badop end + if body.ct > co.srv.cfg.maxupsz then + 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") + return + end + var id = co.srv:artifact_instantiate(body,mime) + if id == 0 then + co:complain(500,'upload failed','artifact rejected. either the server is running out of space or this file is banned from the server') + return + end + co.srv:artifact_expropriate(co.who.id,id,desc,folder) + + var idbuf: int8[lib.math.shorthand.maxlen] + var idlen = lib.math.shorthand.gen(id,&idbuf[0]) + + var url = lib.str.acc{}:compose('/media/a/',pstring{&idbuf[0],idlen}):finalize() + co:reroute(url.ptr) + url:free() + else goto badop end + end + else goto e404 end + do return end + + ::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end + ::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded by this user') return end end do local branches = quote end local filename, flen = symbol(&int8), symbol(intptr) local page = symbol(lib.http.page) @@ -485,10 +552,12 @@ http.actor_profile_uid(co, path, meth) elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then http.tweet_page(co, path, meth) elseif path(0):cmp(lib.str.lit('tl')) then http.timeline(co, path) + elseif path(0):cmp(lib.str.lit('media')) then + http.media_manager(co, path, meth) elseif path(0):cmp(lib.str.lit('doc')) then if not meth_get(meth) then goto wrongmeth end http.documentation(co, path) elseif path(0):cmp(lib.str.lit('conf')) then if co.aid == 0 then goto unauth end Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -136,10 +136,12 @@ who: &lib.store.actor -- who we're logged in as, if aid ~= 0 peer: lib.store.inet reqtype: lib.http.mime.t -- negotiated content type method: lib.http.method.t live_last: lib.store.timepoint + uploads: lib.mem.vec(lib.http.upload) + body: lib.str.t -- cache ui_hue: uint16 navbar: lib.mem.ptr(int8) actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries -- private @@ -330,14 +332,10 @@ terra convo:pgetv(name: rawstring) var s,l = self:getv(name) return pstring { ptr = s, ct = l } end -local urimatch = macro(function(uri, ptn) - return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1) -end) - local route = {} -- these are defined in route.t, as they need access to renderers terra route.dispatch_http :: {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {} local mimetypes = { {'html', 'text/html'}; @@ -393,10 +391,11 @@ } co.varbuf.ptr = nil co.navbar.ptr = nil co.actorcache.top = 0 co.actorcache.cur = 0 co.ui_hue = server.cfg.ui_hue + co.body.ptr = msg.body.ptr co.body.ct = msg.body.len -- first, check for an accept header. if it's there, we need to -- iterate over the values and pick the highest-priority one do var acc = lib.http.findheader(msg, 'Accept') -- TODO handle q-value @@ -513,24 +512,132 @@ else uri.ct = urideclen end lib.dbg('routing URI ', {uri.ptr, uri.ct}) if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then co.method = [lib.http.method.get] - route.dispatch_http(&co, uri, [lib.http.method.get]) elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then - co.method = [lib.http.method.get] - route.dispatch_http(&co, uri, [lib.http.method.post]) + co.method = [lib.http.method.post] elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then co.method = [lib.http.method.head] - route.dispatch_http(&co, uri, [lib.http.method.head]) elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then co.method = [lib.http.method.options] - route.dispatch_http(&co, uri, [lib.http.method.options]) else co:complain(400,'unknown method','you have submitted an invalid http request') + goto fail + end + -- check for a content-type header, and see if it's a multipart/ + -- form-data encoded POST request so we can handle file uploads + co.uploads.sz = 0 co.uploads.run = 0 + if co.method == [lib.http.method.post] then + var ctt = lib.http.findheader(msg, 'Content-Type') + if ctt ~= nil then + lib.dbg('found content type', {ctt.ptr,ctt.ct}) + if lib.str.ncmp(ctt.ptr,'multipart/form-data;',20) == 0 then + var p = lib.str.ffw(ctt.ptr + 20,ctt.ct-20) + if lib.str.ncmp(p,'boundary=',9) ~= 0 then + co:complain(400,'bad request','unrecognized content-type') + goto fail + end + var boundary = pstring {ptr=p+9,ct=ctt.ct - ((p - ctt.ptr) + 9)} + lib.dbg('got boundary ',{boundary.ptr,boundary.ct}) + co.method = lib.http.method.post_file + co.uploads:init(8) + + var bsr = (lib.str.acc{}):compose('\r\n--',boundary,'\r\n'):finalize() + + var upmap = lib.str.splitmap(co.body,bsr,8) + -- first entry may not be preceded by header-break + if lib.str.find(upmap(0), pstring { + ptr = bsr.ptr + 2, ct = bsr.ct - 2 + }):ref() then + upmap(0).ptr = upmap(0).ptr + (bsr.ct - 2) + upmap(0).ct = upmap(0).ct - (bsr.ct - 2) + end + + -- last entry is weird + do var lsr = (lib.str.acc{}):compose('\r\n--',boundary,'--\r\n'):finalize() + var lsent = upmap.ptr + (upmap.ct - 1) + var halt = lib.str.find(@lsent, lsr) + if halt:ref() then + lsent.ct = halt.ptr - lsent.ptr + end + lsr:free() end + + for i=0,upmap.ct do + var hdrbrk = lib.str.find(upmap(i), lib.str.plit'\r\n\r\n') + if hdrbrk:ref() then + lib.dbg('got new entry') + var hdrtxt = pstring {upmap(i).ptr,upmap(i).ct - hdrbrk.ct} + var hdrs = lib.str.splitmap(hdrtxt, '\r\n',6) + var ctt = pstring.null() + var ctd = pstring.null() + for j=0, hdrs.ct do + var brk = lib.str.find(hdrs(j),lib.str.plit':') + if brk:ref() then + var hdr = pstring{hdrs(j).ptr,hdrs(j).ct - brk.ct} + var val = pstring{brk.ptr+1, brk.ct-1}:ffw() + if hdr:cmp(lib.str.plit'Content-Type') then + ctt = val + elseif hdr:cmp(lib.str.plit'Content-Disposition') then + ctd = val + end + end + end + if ctd:ref() then + var ctdvals = lib.str.splitmap(ctd, ';', 4) defer ctdvals:free() + if ctdvals(0):cmp(lib.str.plit'form-data') and ctdvals.ct > 1 then + lib.dbg('found form data') + var fld = pstring.null() + var file = pstring.null() + for j=1, ctdvals.ct do var v = ctdvals(j):ffw() + var x = lib.str.find(v,lib.str.plit'=') + if x:ref() then + var key = pstring{v.ptr, v.ct - x.ct} + var val = pstring{x.ptr + 1, x.ct - 1} + var decval, ofs, sp = lib.str.toknext(val,@';',true) + if key:cmp(lib.str.plit'name') then + fld = decval + elseif key:cmp(lib.str.plit'filename') then + file = decval + else decval:free() end + end + end + if fld:ref() then + var nextup = co.uploads:new() + if ctt:ref() then + nextup.ctype = ctt + else + nextup.ctype = pstring.null() + end + nextup.body = pstring { + ptr = hdrbrk.ptr + 4; + ct = hdrbrk.ct - 4; + } + nextup.ctype = ctt + nextup.field = fld + nextup.filename = file + end + end + end + end + end + bsr:free() + upmap:free() + end + end + end + + route.dispatch_http(&co, uri, co.method) + if co.uploads.run > 0 then + for i=0,co.uploads.sz do + co.uploads(i).filename:free() + co.uploads(i).field:free() + end + co.uploads:free() end + ::fail:: if co.aid ~= 0 then lib.mem.heapf(co.who) end if co.varbuf.ptr ~= nil then co.varbuf:free() end if co.navbar.ptr ~= nil then co.navbar:free() end co.actorcache:free() end ADDED static/file.svg Index: static/file.svg ================================================================== --- static/file.svg +++ static/file.svg @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -1004,5 +1004,67 @@ margin: 0.1in 0.2in; margin-left: 0.4in; } } } + +.media.manager main, .media.gallery { + display: grid; + grid-template-columns: 2in 1fr; + grid-template-rows: max-content 1fr; + menu { + @extend %navmenu; + } + .gallery, .dir { + background: tone(-55%,-0.5); + border: 1px solid tone(-60%); + padding: 0.2in; + display: flex; + flex-wrap: wrap; + } + .gallery { + grid-row: 1/2; grid-column: 2/3; + margin-left: 0.1in; + flex-flow: row; + > a[href].thumb { + display: block; + width: 1.5in; + padding: 0.1in; + height: max-content; + > img { + width: 1.5in; height: 1.5in; + } + > .caption { + text-align: center; + font-size: 80%; + } + } + } + .dir { + grid-row: 2/3; grid-column: 1/3; + margin-top: 0.1in; + flex-flow: column; + flex-grow: 1; + > a[href].file { + padding: 0.1in 0.15in; + text-decoration: none; + height: max-content; + background-image: url(/s/file.webp); //TODO different icons for different mime types + background-repeat: no-repeat; + background-position: left; + padding-left: 0.4in; + > .label { + text-decoration: underline; + } + > .mime { + font-style: italic; + opacity: 60%; + margin-left: 0.5ex; + } + } + } +} + +.media.upload form { + padding: 0.1in 0.2in; + @extend %box; +} Index: store.t ================================================================== --- store.t +++ store.t @@ -228,10 +228,19 @@ isreply: bool source: &m.source -- save :: bool -> {} (defined in acl.t due to dep. hell) } + +struct m.artifact { + rid: uint64 + owner: uint64 + desc: str + folder: str + mime: str + url: str +} m.user_conf_funcs = function(be,n,ty,rty,rty2) rty = rty or ty local gt if not rty2 -- what the fuck? @@ -449,15 +458,19 @@ artifact_quicksearch: {&m.source, lib.mem.ptr(uint8)} -> {uint64,bool} -- checks whether a hash is already in the database without uploading -- the entire file to the database server -- hash: bytea --> artifact id (0 if null), suppressed? - artifact_expropriate: {&m.source, uint64, uint64, lib.mem.ptr(int8)} -> {} + artifact_expropriate: {&m.source, uint64, uint64, lib.str.t, lib.str.t} -> {} -- claims an existing artifact for the user's own collection -- uid: uint64 -- artifact id: uint64 -- description: pstring + -- folder: pstring + artifact_claim_alter: {&m.source, uint64, uint64, lib.str.t, lib.str.t} -> {} + -- edits an existing claim to an artifact + -- ibid artifact_disclaim: {&m.source, uint64, uint64} -> {} -- a user disclaims their ownership stake in an artifact, removing it from -- the database entirely if they were the only owner, and removing their -- description of it either way -- uid: uint64 @@ -468,10 +481,19 @@ -- the artifact will be banned and attempts to upload it in the future -- will fail, triggering a report. mainly intended for dealing with spam, -- IP violations, That Which Shall Not Be Named, and various other infohazards. -- artifact id: uint64 -- blacklist: bool + artifact_enum_uid: {&m.source, uint64, lib.str.t} -> lib.mem.lstptr(m.artifact) + -- produces a list of artifacts claimed by a user, optionally + -- restricted by folder (empty string = new only) + artifact_fetch: {&m.source, uint64, uint64} -> lib.mem.ptr(m.artifact) + -- fetch a user's view of an artifact + -- uid: uint64 + -- rid: uint64 + artifact_load: {&m.source, uint64} -> {lib.mem.ptr(uint8),lib.str.t} + -- load the body of an artifact into memory (also returns mime) nkvd_report_issue: {&m.source, &m.kompromat} -> {} -- an incidence of Badthink has been detected. report it immediately -- to the Supreme Soviet nkvd_reports_enum: {&m.source, &m.kompromat} -> lib.mem.ptr(m.kompromat) Index: str.t ================================================================== --- str.t +++ str.t @@ -19,10 +19,21 @@ terralib.types.funcpointer({&rawstring,rawstring},{int},true)); bfmt = terralib.externfunction('sprintf', terralib.types.funcpointer({rawstring,rawstring},{int},true)); span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring); } + +terra m.ffw(str: &int8, maxlen: intptr) + if maxlen == 0 then maxlen = m.sz(str) end + while maxlen > 0 and @str ~= 0 and + (@str == @' ' or @str == @'\t' or @str == @'\n') do + str = str + 1 + maxlen = maxlen - 1 + end + return str +end + do local strptr = (lib.mem.ptr(int8)) local strref = (lib.mem.ref(int8)) local byteptr = (lib.mem.ptr(uint8)) strptr.metamethods.__cast = function(from,to,e) @@ -53,11 +64,15 @@ if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end if self.ptr[i] ~= other.ptr[i] then return false end end return true end - + terra strptr:ffw() + var newp = m.ffw(self.ptr,self.ct) + var newct = self.ct - (newp - self.ptr) + return strptr { ptr = newp, ct = newct } + end strptr.methods.cmpl = macro(function(self,other) return `self:cmp(strptr { ptr = [other:asvalue()], ct = [#(other:asvalue())] }) end) strref.methods.cmpl = macro(function(self,other) return `self:cmp(strref { ptr = [other:asvalue()], ct = [#(other:asvalue())] }) @@ -359,23 +374,79 @@ end end return maxlen end -terra m.ffw(str: &int8, maxlen: intptr) - while maxlen > 0 and @str ~= 0 and - (@str == @' ' or @str == @'\t' or @str == @'\n') do - str = str + 1 - maxlen = maxlen - 1 - end - return str -end - terra m.ffw_unsafe(str: &int8) while @str ~= 0 and (@str == @' ' or @str == @'\t' or @str == @'\n') do str = str + 1 end return str end + +terra m.find(haystack: pstr, needle: pstr): pstr + for i=0,haystack.ct do + for j=0, needle.ct do + if haystack(i + j) ~= needle(j) then goto nomatch end + end + do return pstr { + ptr = haystack.ptr + i; + ct = haystack.ct - i; + } end + ::nomatch::end + return pstr.null() +end + +terra m.splitmap(str: pstr, delim: pstr, expect: uint16) + var vec: lib.mem.vec(pstr) vec:init(expect) + var start = pstr{str.ptr, str.ct} + while true do + var n = m.find(start, delim) + if not n then break end + vec:push(pstr {ptr = start.ptr, ct = start.ct - n.ct}) + n.ptr = n.ptr + delim.ct + n.ct = n.ct - delim.ct + start = n + end + vec:push(start) + return vec:crush() +end + +terra m.toknext(str: m.t, delim: int8, brkspace: bool): {pstr,intptr,bool} + var b: m.acc b:init(48) + var mode: int8 = 0 + var esc = false + var spacebroke = false + var max = 0 + for i=0, str.ct do + max = i + if str(i) == 0 then break + elseif esc == true then b:push(str.ptr + i,1) esc = false + elseif str(i) == @'\\' then esc = true + + elseif mode == 0 and str(i) == delim then break + elseif mode ~= 2 and str(i) == @'"' then + if mode == 1 + then mode = 0 + else mode = 1 + end + elseif mode ~= 1 and str(i) == @"'" then + if mode == 2 + then mode = 0 + else mode = 2 + end + + elseif brkspace and mode == 0 and ( + str(i) == @' ' or str(i) == @'\t' or + str(i) == @'\r' or str(i) == @'\n') then + spacebroke = true + break + + else b:push(str.ptr + i,1) end + end + if mode ~= 0 then return m.t.null(), 0, false end + + return b:finalize(), max, spacebroke +end return m Index: view/load.lua ================================================================== --- view/load.lua +++ view/load.lua @@ -8,10 +8,13 @@ 'confirm'; 'tweet'; 'profile'; 'compose'; 'notice'; + + 'media-gallery'; + 'media-upload'; 'login-username'; 'login-challenge'; 'conf'; ADDED view/media-gallery.tpl Index: view/media-gallery.tpl ================================================================== --- view/media-gallery.tpl +++ view/media-gallery.tpl @@ -0,0 +1,19 @@ +@menu + new uploads + unfiled +
+ @folders + all uploads + all images + all videos + all text files + all others +
+ +
+ @directory +
+ + ADDED view/media-upload.tpl Index: view/media-upload.tpl ================================================================== --- view/media-upload.tpl +++ view/media-upload.tpl @@ -0,0 +1,22 @@ +
+
+ + +
+
+ + +
+
+ + +
+ + + cancel + +
+ + + @folders + DELETED view/media.tpl Index: view/media.tpl ================================================================== --- view/media.tpl +++ view/media.tpl @@ -1,12 +0,0 @@ - - new uploads - @folders - - -
- @images -
- -
- @files -