;; Setup 安装向导:环境检测、步骤状态、最终落库(设计文档 §3)
(defmodule lamina_setup
(export all))
(defun state-key () 'lamina_setup_state)
(defun installed-flag () "data/installed.flag")
(defun installed? ()
(case (file:read_file_info (installed-flag))
(`#(ok ,_) 'true)
(_ 'false)))
;;---- 环境检测(向导第 1 步)----
(defun check-env ()
(list (check-otp)
(check-lfe)
(check-fossil)
(check-sqlite)
(check-repo-dir (default-repo-dir))))
(defun check-otp ()
(let ((release (erlang:system_info (quote otp_release))))
(make-check "Erlang/OTP" 'true (lists:flatten (io_lib:format "OTP ~s" (list release))))))
(defun check-lfe ()
(let ((apps (application:loaded_applications)))
(if (=:= 'false (lists:keyfind 'lfe 1 apps))
(make-check "LFE" 'false "LFE 未加载")
(let ((ver (element 3 (lists:keyfind 'lfe 1 apps))))
(make-check "LFE" 'true (lists:flatten (io_lib:format "LFE ~s" (list ver))))))))
(defun check-fossil ()
(let ((bin (lamina_config:fossil-bin)))
(if (is_atom bin)
(make-check "fossil" 'false "PATH 中未找到 fossil")
(let ((out (os:cmd (lists:flatten (io_lib:format "~s version" (list bin))))))
(make-check "fossil" 'true (first-line out))))))
(defun check-sqlite ()
(let ((probe (filename:join (tmpdir) "probe.db")))
(case (esqlite3:open probe)
(`#(ok ,conn)
(let ((r (catch (esqlite3:exec conn "CREATE TABLE probe(a);"))))
(esqlite3:close conn)
(file:delete probe)
(make-check "SQLite3" (=:= 'ok r) "esqlite 可写")))
(other
(make-check "SQLite3" 'false (lists:flatten (io_lib:format "~p" (list other))))))))
(defun check-repo-dir (dir)
(let ((probe (filename:join dir "lamina-write-probe")))
(filelib:ensure_dir probe)
(case (file:write_file probe #"ok")
('ok
(file:delete probe)
(make-check "仓库存储目录" 'true dir))
(`#(error ,reason)
(make-check "仓库存储目录" 'false
(lists:flatten (io_lib:format "~s 不可写: ~p" (list dir reason))))))))
(defun make-check (name ok detail)
(maps:from_list (list (tuple 'name name) (tuple 'ok ok) (tuple 'detail detail))))
(defun all-ok? (checks)
(lists:all (lambda (c) (maps:get 'ok c)) checks))
;;---- 向导状态 ----
(defun get-state () (persistent_term:get (state-key) #M()))
(defun put-state (state) (persistent_term:put (state-key) state))
(defun current-step () (maps:get 'step (get-state) 1))
(defun advance (state next-step)
(put-state (maps:put 'step next-step state)))
;;---- 各步校验与保存 ----
;; 第 2 步:站点设置
(defun save-site (state params)
(let ((site-name (param params "site_name"))
(site-url (param params "site_url")))
(if (or (=:= 'undefined site-name) (=:= 'undefined site-url)
(=:= #"" site-name) (=:= #"" site-url))
`#(error "站点名称与 URL 为必填项")
(let ((s1 (maps:put 'site_name (to-str site-name) state)))
(progn (advance (maps:put 'site_url (to-str site-url) s1) 3) 'ok)))))
;; 第 3 步:数据库
(defun save-db (state params)
(let ((db-path (case (param params "db_path")
('undefined "data/lamina.db")
(#"" "data/lamina.db")
(p (to-str p)))))
(case (lamina_db:open db-path)
(`#(ok ,_)
(progn (advance (maps:put 'db_path db-path state) 4) 'ok))
(other
`#(error ,(lists:flatten (io_lib:format "数据库不可创建/不可写: ~p" (list other))))))))
;; 第 4 步:仓库存储目录
(defun save-storage (state params)
(let ((repo-dir (case (param params "repo_dir")
('undefined (default-repo-dir))
(#"" (default-repo-dir))
(p (to-str p)))))
(let ((check (check-repo-dir repo-dir)))
(if (maps:get 'ok check)
(progn (advance (maps:put 'repo_dir repo-dir state) 5) 'ok)
`#(error ,(maps:get 'detail check))))))
;; 第 5 步:管理员账号
(defun save-admin (state params)
(let ((username (param params "username"))
(password (param params "password"))
(password2 (param params "password2")))
(cond
((or (=:= 'undefined username) (=:= username #"") (=:= 'undefined password) (=:= password #""))
`#(error "用户名与密码为必填项"))
((not (=:= password password2))
`#(error "两次输入的密码不一致"))
((< (byte_size password) 8)
`#(error "密码至少需要 8 个字符"))
('true
(let ((s1 (maps:put 'username (to-str username) state)))
(advance (maps:put 'password_hash (lamina_crypto:hash_password password) s1) 6)
'ok)))))
;;---- 第 6 步:完成(写配置/建管理员/锁文件)----
(defun finalize (state)
(let ((site-name (maps:get 'site_name state "lamina"))
(site-url (maps:get 'site_url state "http://localhost:8827"))
(db-path (maps:get 'db_path state "data/lamina.db"))
(repo-dir (maps:get 'repo_dir state (default-repo-dir))))
(let ((conf-content
(lists:flatten
(io_lib:format "{site_name, \"~s\"}.~n{site_url, \"~s\"}.~n{db_path, \"~s\"}.~n{repo_dir, \"~s\"}.~n"
(list (esc site-name) (esc site-url) db-path repo-dir)))))
(case (file:write_file (default-conf-path) conf-content)
('ok
(case (lamina_crypto:load_or_create_key "data/secret.key")
(`#(ok ,_)
(let ((admin-result (insert-admin state)))
(if (=:= 'ok admin-result)
(let ((flag-result (file:write_file (installed-flag) #"installed")))
(case flag-result
('ok
(progn (lamina_config:reload)
(lamina_sup:ensure-runtime)
(persistent_term:erase (state-key))
'ok))
(`#(error ,reason)
`#(error ,(lists:flatten (io_lib:format "写安装标记失败: ~p" (list reason)))))))
admin-result)))
(other other)))
(`#(error ,reason)
`#(error ,(lists:flatten (io_lib:format "写配置文件失败: ~p" (list reason)))))))))
(defun insert-admin (state)
(let ((username (maps:get 'username state))
(hash (maps:get 'password_hash state))
(now (iso8601 (os:system_time (quote second)))))
(case (catch (lamina_db:q "INSERT INTO admin_user(username,password_hash,created_at,updated_at) VALUES(?,?,?,?);"
(list username hash now now)))
(`#(error ,reason) `#(error ,(lists:flatten (io_lib:format "写管理员失败: ~p" (list reason)))))
(_ 'ok))))
;;---- helpers ----
(defun param (params key)
(case (maps:find (list_to_binary key) params)
(`#(ok ,v) v)
('error 'undefined)))
(defun to-str (v) (binary_to_list v))
(defun esc (s)
(string:replace s "\"" "\\\"" 'all))
(defun iso8601 (unix-seconds)
(calendar:system_time_to_rfc3339 unix-seconds (list (tuple 'unit 'second))))
(defun default-conf-path () (lamina_config:default-conf-path))
(defun default-repo-dir () "data/repos")
(defun tmpdir ()
(let* ((cwd (element 2 (file:get_cwd)))
(base (filename:join cwd "_build/tmp")))
(filelib:ensure_dir (filename:join base "x"))
base))
(defun first-line (out)
(hd (string:split out "\n" 'leading)))