{{title}}
-{{{body}}}
- {{#attachment}}{{{this}}}{{/attachment}} +{{title}}
{{{body}}}
diff --git a/public/misc.css b/public/misc.css index 2964358..27ce0b5 100644 --- a/public/misc.css +++ b/public/misc.css @@ -1,16 +1,17 @@ .posts { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-template-rows: repeat(auto-fill, 300px); } .post { margin: 1em; border: solid var(--accent1) 2px; - padding: 1em; overflow-wrap: break-word; + background-size: cover; } -.post > h2 > a { +.post h2 > a { color: inherit; } @@ -19,3 +20,12 @@ margin-bottom: 1em; } +.preview { + padding: 1em; + background: #111111bb; +} + +.attachment { + width: 100%; +} + diff --git a/readme.md b/readme.md index ab4f9c6..4552967 100644 --- a/readme.md +++ b/readme.md @@ -8,7 +8,7 @@ they find throughout the day. Name comes from banal and otiose. - [x] authentication - [x] user config - [x] basic posting -- [ ] media posts +- [x] media posts - [ ] post config - [ ] user profiles - [ ] tagging diff --git a/routes/create.js b/routes/create.js index 0479aee..cd44b7a 100644 --- a/routes/create.js +++ b/routes/create.js @@ -1,22 +1,19 @@ import busboy from "busboy"; -// TODO: this code is messy! redo it async function parse(req, files) { const bb = busboy({ headers: req.headers, limits: { files: 1 } }); return new Promise((res) => { const post = {}; let hasFile = false; bb.on("field", (key, val) => post[key] = val); - bb.on("file", async (_name, file, info) => { + bb.on("file", async (_name, stream, info) => { hasFile = true; - res({ - post, - file: { - attachHash: await files.insert(file), - attachName: info.filename, - attachType: info.mimeType, - }, - }); + const file = { + attachHash: await files.insert(stream), + attachName: info.filename, + attachType: info.mimeType, + }; + res({ post, file }); }); bb.on("close", () => { if(!hasFile) res({ post }); diff --git a/server/database.js b/server/database.js index b86720d..9f997d2 100644 --- a/server/database.js +++ b/server/database.js @@ -25,9 +25,14 @@ async function init(schema, log) { table.integer("author").unsigned(); table.string("title"); table.string("body"); - table.string("attachHash"); - table.string("attachName"); - table.string("attachType"); + table.integer("attachment").unsigned(); + }).createTable("files", table => { + // map of the files + table.increments("fileId").primary(); + table.string("hash"); + table.string("name"); + table.string("type"); + table.binary("preview"); }).createTable("log", table => { // log of everything that happens table.increments("auditId"); diff --git a/server/files.js b/server/files.js index a60364f..7e8604c 100644 --- a/server/files.js +++ b/server/files.js @@ -1,3 +1,4 @@ +// save files to a hash-based filestore import fs from "fs/promises"; import path from "path"; import crypto from "crypto"; @@ -5,9 +6,9 @@ import crypto from "crypto"; class Filestore { where = path.join(process.cwd(), ".data"); - constructor(log, db, options) { - this.log = log; - this.db = db; + constructor(ctx, options) { + this.log = ctx.log; + this.db = ctx.db; this.options = options; } @@ -28,7 +29,7 @@ class Filestore { const h = hash.digest(); await tmpFd.close(); await fs.rename(tmpName, path.join(this.where, "files", h.toString("hex"))); - this.log.info("saved file with hash " + h.toString("hex")); + this.log.debug("saved file with hash " + h.toString("hex")); res(h); }); }); @@ -47,6 +48,6 @@ async function exists(where) { export default async function(ctx, options) { if(!await exists(".data/files")) await fs.mkdir(".data/files"); if(!await exists(".data/tmp")) await fs.mkdir(".data/tmp"); - return new Filestore(ctx.log, ctx.db, options); + return new Filestore(ctx, options); } diff --git a/server/posts.js b/server/posts.js index 483b18b..af524de 100644 --- a/server/posts.js +++ b/server/posts.js @@ -6,16 +6,19 @@ const md = markdown({ linkify: true, }); +// get a post by id export async function getPost(db, id) { const [post] = await db("posts").select().where("postId", id); return post ?? null; } +// get a user by id export async function getUser(db, id) { const [user] = await db("users").select().where("userId", id); return user ?? null; } +// render a post export function render(post, author, trim = false) { const date = new Date(post.createdAt); const body = (trim && post.body.length > 200) ? post.body.slice(0, 200) + "..." : post.body; @@ -30,13 +33,36 @@ export function render(post, author, trim = false) { }; } -// TODO: add support for other content types -export function attachment(post) { - switch(post.attachType?.split("/")[0]) { - case "image": - return ``; - default: - return null; +// render a post's attachment to html +function attachment(post) { + if(!post.attachHash) return null; + const location = `/media/${post.attachHash.toString("hex")}/${escape(post.attachName)}`; + return { + html: toHtml(), + preview: getPreview(), + url: location, + }; + + function toHtml() { + switch(post.attachType.split("/")[0]) { + case "image": + return ``; + case "audio": + return ``; + case "video": + return ``; + default: + return null; + } + } + + function getPreview() { + switch(post.attachType.split("/")[0]) { + case "image": + return location; + default: + return null; + } } } @@ -57,6 +83,7 @@ const units = [ { name: "internet explorer page load", size: 1000 }, ]; +// format dates to a relative time function format(date) { const delta = Date.now() - date; let size = 1; diff --git a/views/partials/post.html b/views/partials/post.html index 8adbaed..1e6f902 100644 --- a/views/partials/post.html +++ b/views/partials/post.html @@ -1,6 +1,7 @@ -
{{{body}}}
- {{#attachment}}{{{this}}}{{/attachment}} +{{{body}}}