///|
/// 元数据存储:纯文本 JSON 文件(`meta/config.json` + `meta/api_keys.json`)。
/// 不用 sqlite:减少依赖、文件可 git、可回退;单进程规模足够。
/// 写文件用「临时文件 + rename」保证原子性。
struct JsonMeta {
dir : String
}
///|
/// 打开元数据目录(不存在则自动创建)
pub async fn JsonMeta::open(dir : String) -> JsonMeta {
if !@fs.exists(dir) {
@fs.mkdir(dir, recursive=true)
}
{ dir, }
}
///|
fn JsonMeta::config_path(self : JsonMeta) -> String {
self.dir + "/config.json"
}
///|
fn JsonMeta::api_keys_path(self : JsonMeta) -> String {
self.dir + "/api_keys.json"
}
///|
fn JsonMeta::users_path(self : JsonMeta) -> String {
self.dir + "/users.json"
}
///|
fn JsonMeta::categories_path(self : JsonMeta) -> String {
self.dir + "/categories.json"
}
///|
fn JsonMeta::links_path(self : JsonMeta) -> String {
self.dir + "/links.json"
}
///|
async fn JsonMeta::load_config(self : JsonMeta) -> Map[String, String] {
let path = self.config_path()
guard @fs.exists(path) else { return {} }
let content = @utf8.decode_lossy(@fs.read_file(path).binary()[:])
let json = @json.parse(content) catch { _ => return {} }
let result : Map[String, String] = Map([])
if json is Object(obj) {
for k, v in obj {
if v is String(s) {
result[k] = s
}
}
}
result
}
///|
async fn JsonMeta::save_config(
self : JsonMeta,
config : Map[String, String],
) -> Unit {
let obj : Map[String, Json] = Map([])
for k, v in config {
obj[k] = Json::string(v)
}
let content = Json::object(obj).stringify(indent=2)
let tmp = self.config_path() + ".tmp"
@fs.write_file(tmp, content)
@fs.rename(tmp, self.config_path(), replace=true)
}
///|
/// 读配置项,不存在返回 None
pub async fn JsonMeta::get_config(self : JsonMeta, key : String) -> String? {
self.load_config().get(key)
}
///|
/// 写配置项
pub async fn JsonMeta::set_config(
self : JsonMeta,
key : String,
value : String,
) -> Unit {
let config = self.load_config()
config[key] = value
self.save_config(config)
}
///|
pub struct ApiKeyRecord {
id : Int
name : String
key_hash : String
scopes : String
user_id : Int
read_prefix : String
write_prefix : String
mut enabled : Bool
created_at : String
}
///|
pub struct ApiKeyInfo {
id : Int
name : String
scopes : String
user_id : Int
read_prefix : String
write_prefix : String
enabled : Bool
created_at : String
}
///|
fn json_str(o : Map[String, Json], key : String) -> String {
match o.get(key) {
Some(String(s)) => s
_ => ""
}
}
///|
fn json_int(o : Map[String, Json], key : String) -> Int {
match o.get(key) {
Some(Number(n, repr=_)) => n.to_int()
_ => 0
}
}
///|
fn json_bool(o : Map[String, Json], key : String) -> Bool {
match o.get(key) {
Some(True) => true
_ => false
}
}
///|
async fn JsonMeta::load_api_keys(self : JsonMeta) -> Array[ApiKeyRecord] {
let path = self.api_keys_path()
guard @fs.exists(path) else { return [] }
let content = @utf8.decode_lossy(@fs.read_file(path).binary()[:])
let json = @json.parse(content) catch { _ => return [] }
let result : Array[ApiKeyRecord] = []
if json is Array(items) {
for item in items {
if item is Object(o) {
result.push({
id: json_int(o, "id"),
name: json_str(o, "name"),
key_hash: json_str(o, "key_hash"),
scopes: json_str(o, "scopes"),
user_id: json_int(o, "user_id"),
read_prefix: json_str(o, "read_prefix"),
write_prefix: json_str(o, "write_prefix"),
enabled: json_bool(o, "enabled"),
created_at: json_str(o, "created_at"),
})
}
}
}
result
}
///|
async fn JsonMeta::save_api_keys(
self : JsonMeta,
keys : Array[ApiKeyRecord],
) -> Unit {
let arr : Array[Json] = []
for k in keys {
let obj : Map[String, Json] = {
"id": Json::number(k.id.to_double()),
"name": Json::string(k.name),
"key_hash": Json::string(k.key_hash),
"scopes": Json::string(k.scopes),
"user_id": Json::number(k.user_id.to_double()),
"read_prefix": Json::string(k.read_prefix),
"write_prefix": Json::string(k.write_prefix),
"enabled": Json::boolean(k.enabled),
"created_at": Json::string(k.created_at),
}
arr.push(Json::object(obj))
}
let content = Json::array(arr).stringify(indent=2)
let tmp = self.api_keys_path() + ".tmp"
@fs.write_file(tmp, content)
@fs.rename(tmp, self.api_keys_path(), replace=true)
}
///|
pub struct UserRecord {
id : Int
username : String
mut password_hash : String
mut role : String
mut enabled : Bool
mut write_prefix : String
}
///|
pub struct CategoryRecord {
id : Int
name : String
slug : String
parent_id : Int
description : String
}
///|
async fn JsonMeta::load_categories(self : JsonMeta) -> Array[CategoryRecord] {
let path = self.categories_path()
guard @fs.exists(path) else { return [] }
let content = @utf8.decode_lossy(@fs.read_file(path).binary()[:])
let json = @json.parse(content) catch { _ => return [] }
let result : Array[CategoryRecord] = []
if json is Array(items) {
for item in items {
if item is Object(o) {
result.push({
id: json_int(o, "id"),
name: json_str(o, "name"),
slug: json_str(o, "slug"),
parent_id: json_int(o, "parent_id"),
description: json_str(o, "description"),
})
}
}
}
result
}
///|
async fn JsonMeta::save_categories(
self : JsonMeta,
categories : Array[CategoryRecord],
) -> Unit {
let items : Array[Json] = []
for c in categories {
items.push(
Json::object({
"id": Json::number(c.id.to_double()),
"name": Json::string(c.name),
"slug": Json::string(c.slug),
"parent_id": Json::number(c.parent_id.to_double()),
"description": Json::string(c.description),
}),
)
}
let tmp = self.categories_path() + ".tmp"
@fs.write_file(tmp, Json::array(items).stringify(indent=2))
@fs.rename(tmp, self.categories_path(), replace=true)
}
///|
pub async fn JsonMeta::list_categories(
self : JsonMeta,
) -> Array[CategoryRecord] {
self.load_categories()
}
///|
pub async fn JsonMeta::create_category(
self : JsonMeta,
name : String,
slug : String,
parent_id : Int,
description : String,
) -> Bool {
let categories = self.load_categories()
for c in categories {
if c.slug == slug {
return false
}
}
let next_id = categories.fold(init=0, fn(max_id, c) {
if c.id > max_id {
c.id
} else {
max_id
}
}) +
1
categories.push({ id: next_id, name, slug, parent_id, description, })
self.save_categories(categories)
true
}
///|
async fn JsonMeta::load_users(self : JsonMeta) -> Array[UserRecord] {
let path = self.users_path()
guard @fs.exists(path) else { return [] }
let content = @utf8.decode_lossy(@fs.read_file(path).binary()[:])
let json = @json.parse(content) catch { _ => return [] }
let users : Array[UserRecord] = []
if json is Array(items) {
for item in items {
if item is Object(o) {
users.push({
id: json_int(o, "id"),
username: json_str(o, "username"),
password_hash: json_str(o, "password_hash"),
role: json_str(o, "role"),
enabled: json_bool(o, "enabled"),
write_prefix: json_str(o, "write_prefix"),
})
}
}
}
users
}
///|
async fn JsonMeta::save_users(
self : JsonMeta,
users : Array[UserRecord],
) -> Unit {
let items : Array[Json] = []
for user in users {
items.push(
Json::object({
"id": Json::number(user.id.to_double()),
"username": Json::string(user.username),
"password_hash": Json::string(user.password_hash),
"role": Json::string(user.role),
"enabled": Json::boolean(user.enabled),
"write_prefix": Json::string(user.write_prefix),
}),
)
}
let tmp = self.users_path() + ".tmp"
@fs.write_file(tmp, Json::array(items).stringify(indent=2))
@fs.rename(tmp, self.users_path(), replace=true)
}
///|
pub async fn JsonMeta::find_user_by_id(
self : JsonMeta,
id : Int,
) -> UserRecord? {
for user in self.load_users() {
if user.id == id && user.enabled {
return Some(user)
}
}
None
}
///|
pub async fn JsonMeta::find_user(
self : JsonMeta,
username : String,
) -> UserRecord? {
for user in self.load_users() {
if user.username == username && user.enabled {
return Some(user)
}
}
None
}
///|
pub async fn JsonMeta::list_users(self : JsonMeta) -> Array[UserRecord] {
self.load_users()
}
///|
pub async fn JsonMeta::create_user(
self : JsonMeta,
username : String,
password : String,
role : String,
write_prefix : String,
) -> Bool {
let users = self.load_users()
for user in users {
if user.username == username {
return false
}
}
let next_id = users.fold(init=0, fn(max_id, user) {
if user.id > max_id {
user.id
} else {
max_id
}
}) +
1
users.push({
id: next_id,
username,
password_hash: hash_api_key(password),
role,
enabled: true,
write_prefix,
})
self.save_users(users)
true
}
///|
pub async fn JsonMeta::reset_user_password(
self : JsonMeta,
id : Int,
password : String,
) -> Bool {
let users = self.load_users()
let mut found = false
for i in 0.. Bool {
let users = self.load_users()
let mut found = false
for i in 0.. UserRecord? {
let hash = hash_api_key(password)
for user in self.load_users() {
if user.username == username && user.enabled && user.password_hash == hash {
return Some(user)
}
}
None
}
///|
/// 创建 API key:返回明文 key(仅此一次,文件里只存哈希)
pub async fn JsonMeta::create_api_key(
self : JsonMeta,
name : String,
scopes : String,
user_id : Int,
read_prefix : String,
write_prefix : String,
) -> String {
let keys = self.load_api_keys()
let next_id = keys.fold(init=0, fn(acc, k) {
if k.id > acc {
k.id
} else {
acc
}
}) +
1
let plain = generate_api_key()
let record = {
id: next_id,
name,
key_hash: hash_api_key(plain),
scopes,
user_id,
read_prefix,
write_prefix,
enabled: true,
created_at: "",
}
keys.push(record)
self.save_api_keys(keys)
plain
}
///|
/// 校验 API key:有效时返回完整绑定记录。
pub async fn JsonMeta::verify_api_key(
self : JsonMeta,
key : String,
) -> ApiKeyRecord? {
let hash = hash_api_key(key)
for record in self.load_api_keys() {
if record.key_hash == hash && record.enabled {
return Some(record)
}
}
None
}
///|
/// 列出全部 API key(不含哈希)
pub async fn JsonMeta::list_api_keys(self : JsonMeta) -> Array[ApiKeyInfo] {
let result : Array[ApiKeyInfo] = []
for r in self.load_api_keys() {
result.push({
id: r.id,
name: r.name,
scopes: r.scopes,
user_id: r.user_id,
read_prefix: r.read_prefix,
write_prefix: r.write_prefix,
enabled: r.enabled,
created_at: r.created_at,
})
}
result
}
///|
/// 吊销 API key(软删除:enabled = false)
pub async fn JsonMeta::revoke_api_key(self : JsonMeta, id : Int) -> Unit {
let keys = self.load_api_keys()
for i in 0.. 引用它的文档 slug 列表`。
async fn JsonMeta::load_links(self : JsonMeta) -> Map[String, Array[String]] {
let path = self.links_path()
guard @fs.exists(path) else { return Map([]) }
let content = @utf8.decode_lossy(@fs.read_file(path).binary()[:])
let json = @json.parse(content) catch { _ => return Map([]) }
let result : Map[String, Array[String]] = Map([])
if json is Object(o) {
for k, v in o {
if v is Array(arr) {
let list : Array[String] = []
for item in arr {
if item is String(s) {
list.push(s)
}
}
result[k] = list
}
}
}
result
}
///|
/// 保存链接台账。
async fn JsonMeta::save_links(
self : JsonMeta,
links : Map[String, Array[String]],
) -> Unit {
let obj : Map[String, Json] = Map([])
for k, v in links {
let arr : Array[Json] = []
for s in v {
arr.push(Json::string(s))
}
obj[k] = Json::array(arr)
}
let encoded = Json::object(obj).stringify(indent=2)
let tmp = self.links_path() + ".tmp"
@fs.write_file(tmp, encoded)
@fs.rename(tmp, self.links_path(), replace=true)
}
///|
/// 把一个源文档「引用的目标列表」写进台账。
/// 先移除该源文档的旧记录(避免残留),再写入新的 target -> source 反向映射。
pub async fn JsonMeta::record_links(
self : JsonMeta,
source : String,
targets : Array[String],
) -> Unit {
let links = self.load_links()
// 先移除 source 在所有 target 中的旧记录
for target, sources in links {
if sources.contains(source) {
let kept : Array[String] = []
for s in sources {
if s != source {
kept.push(s)
}
}
links[target] = kept
}
}
// 再写入新的 target -> source
for t in targets {
if !t.is_empty() && t != source {
match links.get(t) {
Some(list) =>
if !list.contains(source) {
let new_list = list
new_list.push(source)
links[t] = new_list
}
None => links[t] = [source]
}
}
}
self.save_links(links)
}
///|
/// 查询某 target 的链入页面(引用它的文档 slug 列表)。
pub async fn JsonMeta::backlinks(
self : JsonMeta,
target : String,
) -> Array[String] {
match self.load_links().get(target) {
Some(list) => list
None => []
}
}
///|
/// 移除一个源文档在台账中的所有记录(删除文档时调用)。
pub async fn JsonMeta::remove_source_links(
self : JsonMeta,
source : String,
) -> Unit {
let links = self.load_links()
let mut changed = false
for target, sources in links {
if sources.contains(source) {
let kept : Array[String] = []
for s in sources {
if s != source {
kept.push(s)
}
}
links[target] = kept
changed = true
}
}
if changed {
self.save_links(links)
}
}