Fosi Fossil Service
Small teams. Open source. Built on Fossil.

CaaS / Lamina

Unified Management for Fossil Repositories

Public | Owner: CaaS | Default branch: trunk | Last update: 2026-10-25 00:41

Artifact 8d5c00832eb40b109da833b40e90413f47d5cec2e4c5a6f93c4e724bf2ef626a:


;; 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)))

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/CaaS/Lamina

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.