minoteaur/src/util.nim

90 lines
3.6 KiB
Nim

import times
import unicode
import strutils except splitWhitespace
import sequtils
import tiny_sqlite
import random
import math
import times
import options
import json
from os import `/`, existsOrCreateDir
import md5
import prologue
import logging
func lowercaseFirstLetter(s: string): string =
if len(s) == 0:
return ""
var
rune: Rune
i = 0
fastRuneAt(s, i, rune, doInc = true)
result = $toLower(rune) & substr(s, i)
func pageToSlug*(page: string): string = page.split({'_', ' '}).map(lowercaseFirstLetter).join("_")
func slugToPage*(slug: string): string = slug.split({'_', ' '}).map(capitalize).join(" ")
func timeToTimestamp*(t: Time): int64 = toUnix(t) * 1000 + (nanosecond(t) div 1000000)
func timestampToTime*(ts: int64): Time = initTime(ts div 1000, (ts mod 1000) * 1000000)
func timestampToStr*(t: Time): string = intToStr(int(timeToTimestamp(t)))
proc toJsonHook(t: Time): JsonNode = newJInt(timeToTimestamp(t))
# store time as milliseconds
proc toDbValue*(t: Time): DbValue = DbValue(kind: sqliteInteger, intVal: timeToTimestamp(t))
proc fromDbValue*(value: DbValue, T: typedesc[Time]): Time = timestampToTime(value.intVal)
template autoInitializedThreadvar*(name: untyped, typ: typedesc, initialize: typed): untyped =
var data* {.threadvar.}: Option[typ]
proc `name`*(): typ =
if isSome(data): result = get data
else:
result = initialize
data = some result
# https://github.com/aisk/simpleflake.nim/blob/master/src/simpleflake.nim - unique 64-bit timestamped ID generation
# not actually identical to that as this has 2 bits less randomness to avoid timestamp overflow issues in 2034 (the application is likely to be replaced by 2139 so the new time is probably fine)
# This is a signed integer for SQLite compatibility
const SIMPLEFLAKE_EPOCH = 946702800
const SIMPLEFLAKE_RANDOM_LENGTH = 21
let now = times.getTime()
var rng {.threadvar.}: Rand
var rngInitialized {.threadvar.}: bool
proc snowflake*(): int64 =
if not rngInitialized:
rng = random.initRand((now.toUnix * 1_000_000_000 + now.nanosecond) xor getThreadId())
rngInitialized = true
let now = times.getTime().toUnixFloat()
var ts = int64((now - SIMPLEFLAKE_EPOCH) * 1000)
let randomBits = int64(rng.rand(2 ^ SIMPLEFLAKE_RANDOM_LENGTH))
return ts shl SIMPLEFLAKE_RANDOM_LENGTH or randomBits
# remove any unsafe characters from a filename, by only allowing bytes [a-zA-Z._ -]
proc normalizeFilename(s: string): string =
for byte in s:
if byte in {' ', 'a'..'z', 'A'..'Z', '-', '_', '.'}:
result.add(byte)
else:
result.add('_')
proc makeFilePath*(basepath, page, filename: string): string =
# putting tons of things into one directory may cause issues, so "shard" it into 256 subdirs deterministically
let pageHash = getMD5(page)
let hashdir = pageHash[0..1]
# it is possible that for some horrible reason someone could make two files/pages which normalize to the same thing
# but are nevertheless different files
# thus, put the hash of the ORIGINAL file/pagename before the normalized version
let pagedir = pageHash[2..31] & "-" & normalizeFilename(page)
let filenameHash = getMD5(filename)
discard existsOrCreateDir(basepath / hashdir)
discard existsOrCreateDir(basepath / hashdir / pagedir)
# saved file path should not include the basedir for file storage, as this may be moved around/reconfigured
hashdir / pagedir / (filenameHash & "-" & normalizeFilename(filename))
autoInitializedThreadvar(logger, ConsoleLogger, newConsoleLogger())
type
AppContext* = ref object of Context
db*: DbConn