2021-02-16 14:26:01 +00:00
import times
import unicode
import strutils except splitWhitespace
import sequtils
import tiny_sqlite
import random
import math
import times
import options
2021-03-13 22:36:41 +00:00
import json
from os import ` / ` , existsOrCreateDir
import md5
import prologue
import logging
2021-02-16 14:26:01 +00:00
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 ) ) )
2021-03-13 22:36:41 +00:00
proc toJsonHook ( t : Time ) : JsonNode = newJInt ( timeToTimestamp ( t ) )
2021-02-16 14:26:01 +00:00
# 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 ]
2021-03-13 22:36:41 +00:00
proc `name` * ( ) : typ =
2021-02-16 14:26:01 +00:00
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 ( )
2021-02-17 23:37:42 +00:00
var rng {. threadvar . } : Rand
var rngInitialized {. threadvar . } : bool
2021-02-16 14:26:01 +00:00
proc snowflake * ( ) : int64 =
2021-02-17 23:37:42 +00:00
if not rngInitialized :
rng = random . initRand ( ( now . toUnix * 1_000_000_000 + now . nanosecond ) xor getThreadId ( ) )
rngInitialized = true
2021-02-16 14:26:01 +00:00
let now = times . getTime ( ) . toUnixFloat ( )
var ts = int64 ( ( now - SIMPLEFLAKE_EPOCH ) * 1000 )
let randomBits = int64 ( rng . rand ( 2 ^ SIMPLEFLAKE_RANDOM_LENGTH ) )
2021-03-13 22:36:41 +00:00
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