318 lines
7.6 KiB
Nim
Raw Normal View History

2023-09-28 17:14:27 -07:00
import std/os
import std/options
import std/strutils
2023-09-28 19:00:22 -07:00
import std/tempfiles
2023-09-28 17:14:27 -07:00
import pkg/questionable
import pkg/questionable/results
from pkg/stew/results as stewResults import get, isErr
import pkg/upraises
import ./backend
2023-09-28 18:32:47 -07:00
export backend
2023-09-28 17:14:27 -07:00
push: {.upraises: [].}
2023-09-28 19:00:22 -07:00
import std/sharedtables
2023-09-28 19:17:43 -07:00
type
KeyLock = tuple[locked: bool]
var keyTable: SharedTable[KeyId, KeyLock]
keyTable.init()
2023-09-28 19:25:07 -07:00
template lockKeyImpl(key: KeyId, blk: untyped): untyped =
2023-09-28 19:17:43 -07:00
var hasLock = false
try:
while not hasLock:
keyTable.withKey(key) do (k: KeyId, klock: var KeyLock, exists: var bool):
if not exists or not klock.locked:
klock.locked = true
exists = true
hasLock = klock.locked
`blk`
finally:
if hasLock:
keyTable.withKey(key) do (k: KeyId, klock: var KeyLock, exists: var bool):
assert exists and klock.locked
klock.locked = false
exists = false
2023-09-28 19:00:22 -07:00
2023-09-28 19:25:07 -07:00
template withReadLock(key: KeyId, blk: untyped): untyped =
2023-09-28 19:17:43 -07:00
lockKeyImpl(key, blk)
2023-09-28 19:25:07 -07:00
template withWriteLock(key: KeyId, blk: untyped): untyped =
2023-09-28 19:17:43 -07:00
lockKeyImpl(key, blk)
2023-09-28 19:00:22 -07:00
2023-09-28 17:14:27 -07:00
type
2023-09-28 17:40:19 -07:00
FSBackend*[K, V] = object
2023-09-28 17:14:27 -07:00
root*: DataBuffer
ignoreProtected: bool
2023-09-28 18:32:47 -07:00
depth*: int
2023-09-28 17:14:27 -07:00
proc isRootSubdir*(root, path: string): bool =
path.startsWith(root)
2023-09-28 17:40:19 -07:00
proc validDepth*(self: FSBackend, key: Key): bool =
2023-09-28 17:14:27 -07:00
key.len <= self.depth
2023-09-28 17:40:19 -07:00
proc findPath*[K,V](self: FSBackend[K,V], key: K): ?!string =
2023-09-28 17:14:27 -07:00
## Return filename corresponding to the key
## or failure if the key doesn't correspond to a valid filename
##
let root = $self.root
let key = Key.init($key).get()
if not self.validDepth(key):
return failure "Path has invalid depth!"
var
segments: seq[string]
for ns in key:
let basename = ns.value.extractFilename
if basename == "" or not basename.isValidFilename:
return failure "Filename contains invalid chars!"
if ns.field == "":
segments.add(ns.value)
else:
let basename = ns.field.extractFilename
if basename == "" or not basename.isValidFilename:
return failure "Filename contains invalid chars!"
# `:` are replaced with `/`
segments.add(ns.field / ns.value)
let
fullname = (root / segments.joinPath())
.absolutePath()
.catch()
.get()
.addFileExt(FileExt)
if not root.isRootSubdir(fullname):
return failure "Path is outside of `root` directory!"
return success fullname
2023-09-28 17:40:19 -07:00
proc has*[K,V](self: FSBackend[K,V], key: K): ?!bool =
2023-09-28 17:14:27 -07:00
without path =? self.findPath(key), error:
return failure error
2023-09-28 19:00:22 -07:00
withReadLock(key):
success path.fileExists()
2023-09-28 17:14:27 -07:00
2023-09-28 17:40:19 -07:00
proc contains*[K](self: FSBackend, key: K): bool =
2023-09-28 17:14:27 -07:00
return self.has(key).get()
2023-09-28 17:40:19 -07:00
proc delete*[K,V](self: FSBackend[K,V], key: K): ?!void =
2023-09-28 17:14:27 -07:00
without path =? self.findPath(key), error:
return failure error
if not path.fileExists():
return success()
try:
2023-09-28 19:00:22 -07:00
withWriteLock(key):
removeFile(path)
2023-09-28 17:14:27 -07:00
except OSError as e:
return failure e
return success()
2023-09-28 17:40:19 -07:00
proc delete*[K,V](self: FSBackend[K,V], keys: openArray[K]): ?!void =
2023-09-28 17:14:27 -07:00
for key in keys:
if err =? self.delete(key).errorOption:
return failure err
return success()
2023-09-28 19:25:07 -07:00
proc readFile[K, V](self: FSBackend, key: K, path: string): ?!V =
2023-09-28 17:14:27 -07:00
var
file: File
defer:
file.close
2023-09-28 19:00:22 -07:00
withReadLock(key):
if not file.open(path):
return failure "unable to open file! path: " & path
2023-09-28 17:14:27 -07:00
2023-09-28 19:00:22 -07:00
try:
let
size = file.getFileSize().int
2023-09-28 17:14:27 -07:00
2023-09-28 19:00:22 -07:00
when V is seq[byte]:
var bytes = newSeq[byte](size)
elif V is V:
var bytes = V.new(size=size)
else:
{.error: "unhandled result type".}
var
read = 0
2023-09-28 17:14:27 -07:00
2023-09-28 19:00:22 -07:00
# echo "BYTES: ", bytes.repr
while read < size:
read += file.readBytes(bytes.toOpenArray(0, size-1), read, size)
2023-09-28 17:14:27 -07:00
2023-09-28 19:00:22 -07:00
if read < size:
return failure $read & " bytes were read from " & path &
" but " & $size & " bytes were expected"
2023-09-28 17:14:27 -07:00
2023-09-28 19:00:22 -07:00
return success bytes
2023-09-28 17:14:27 -07:00
2023-09-28 19:00:22 -07:00
except CatchableError as e:
return failure e
2023-09-28 17:14:27 -07:00
2023-09-28 17:40:19 -07:00
proc get*[K,V](self: FSBackend[K,V], key: K): ?!V =
2023-09-28 17:14:27 -07:00
without path =? self.findPath(key), error:
return failure error
if not path.fileExists():
return failure(
newException(DatastoreKeyNotFound, "Key doesn't exist"))
2023-09-28 19:25:07 -07:00
return readFile[K, V](self, key, path)
2023-09-28 17:14:27 -07:00
2023-09-28 17:40:19 -07:00
proc put*[K,V](self: FSBackend[K,V],
2023-09-28 17:14:27 -07:00
key: K,
data: V
): ?!void =
without path =? self.findPath(key), error:
return failure error
try:
var data = data
2023-09-28 19:00:22 -07:00
withWriteLock(KeyId.new path):
createDir(parentDir(path))
2023-09-28 19:25:07 -07:00
let tmpPath = genTempPath("temp", path.splitPath.tail)
2023-09-28 19:00:22 -07:00
writeFile(tmpPath, data.toOpenArray(0, data.len()-1))
withWriteLock(key):
2023-09-28 19:25:07 -07:00
try:
moveFile(tmpPath, path)
except Exception as e:
return failure e.msg
2023-09-28 17:14:27 -07:00
except CatchableError as e:
return failure e
return success()
proc put*[K,V](
2023-09-28 17:40:19 -07:00
self: FSBackend,
2023-09-28 17:14:27 -07:00
batch: seq[DbBatchEntry[K, V]]): ?!void =
for entry in batch:
if err =? self.put(entry.key, entry.data).errorOption:
return failure err
return success()
iterator dirIter(path: string): string {.gcsafe.} =
try:
for p in path.walkDirRec(yieldFilter = {pcFile}, relative = true):
yield p
except CatchableError as exc:
raise newException(Defect, exc.msg)
2023-09-28 17:40:19 -07:00
proc close*[K,V](self: FSBackend[K,V]): ?!void =
2023-09-28 17:14:27 -07:00
return success()
type
FsQueryHandle*[K, V] = object
query*: DbQuery[K]
cancel*: bool
closed*: bool
env*: FsQueryEnv[K,V]
FsQueryEnv*[K,V] = object
2023-09-28 17:40:19 -07:00
self: FSBackend[K,V]
2023-09-28 17:14:27 -07:00
basePath: DataBuffer
proc query*[K,V](
2023-09-28 17:40:19 -07:00
self: FSBackend[K,V],
2023-09-28 17:14:27 -07:00
query: DbQuery[K],
): Result[FsQueryHandle[K, V], ref CatchableError] =
let key = query.key
without path =? self.findPath(key), error:
return failure error
let basePath =
# it there is a file in the directory
# with the same name then list the contents
# of the directory, otherwise recurse
# into subdirectories
if path.fileExists:
path.parentDir
else:
path.changeFileExt("")
let env = FsQueryEnv[K,V](self: self, basePath: DataBuffer.new(basePath))
success FsQueryHandle[K, V](query: query, env: env)
proc close*[K,V](handle: var FsQueryHandle[K,V]) =
if not handle.closed:
handle.closed = true
iterator queryIter*[K, V](
handle: var FsQueryHandle[K, V]
): ?!DbQueryResponse[K, V] =
let root = $(handle.env.self.root)
let basePath = $(handle.env.basePath)
for path in basePath.dirIter():
if handle.cancel:
break
var
basePath = $handle.env.basePath
keyPath = basePath
keyPath.removePrefix(root)
keyPath = keyPath / path.changeFileExt("")
keyPath = keyPath.replace("\\", "/")
let
flres = (basePath / path).absolutePath().catch
if flres.isErr():
yield DbQueryResponse[K,V].failure flres.error()
continue
let
key = K.toKey($Key.init(keyPath).expect("valid key"))
data =
if handle.query.value:
2023-09-28 19:25:07 -07:00
let res = readFile[K, V](handle.env.self, key, flres.get)
2023-09-28 17:14:27 -07:00
if res.isErr():
yield DbQueryResponse[K,V].failure res.error()
continue
res.get()
else:
V.new()
yield success (key.some, data)
handle.close()
2023-09-28 17:40:19 -07:00
proc newFSBackend*[K,V](root: string,
2023-09-28 17:14:27 -07:00
depth = 2,
caseSensitive = true,
ignoreProtected = false
2023-09-28 17:40:19 -07:00
): ?!FSBackend[K,V] =
2023-09-28 17:14:27 -07:00
let root = ? (
block:
if root.isAbsolute: root
else: getCurrentDir() / root).catch
if not dirExists(root):
return failure "directory does not exist: " & root
2023-09-28 17:40:19 -07:00
success FSBackend[K,V](
2023-09-28 17:14:27 -07:00
root: DataBuffer.new root,
ignoreProtected: ignoreProtected,
depth: depth)