feat(link): 連結一律寫成 [文字](絕對網址),寫入前先驗證連得到
取消 [[頁名]] 與 [[顯示文字|頁名]] 兩種同 wiki 寫法,不再分「同存取庫」與 「跨存取庫」兩條規則。那種寫法只在自己那個 wiki 內解析,寫錯不報錯,畫面上 看起來像普通文字或死連結,巡不到也修不了。 連結寫進頁面前先過 jsc-gitea 的 link-check.sh,結束碼 0 才寫。驗證一律走 API, 不看網頁狀態碼:私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把 好連結判成壞的。認證失敗回 7,與死連結的 1 分開,免得金鑰一過期就把還在的頁 整批判死。
This commit is contained in:
+4
-3
@@ -13,7 +13,8 @@
|
||||
# gitea.sh wiki-get <owner>/<repo> <page> # 印出 wiki 頁 markdown;不存在時 exit 4
|
||||
# gitea.sh wiki-put <owner>/<repo> <page> <file> # 建立或更新 wiki 頁(內容取自檔案)
|
||||
# gitea.sh wiki-delete <owner>/<repo> <page> # 刪除 wiki 頁;與 wiki-put 走同一道人工確認
|
||||
# gitea.sh wiki-url <owner>/<repo> <page> # 印出 wiki 頁絕對網址(取自 API 的 html_url);跨存取庫連結用
|
||||
# gitea.sh wiki-url <owner>/<repo> <page> # 印出 wiki 頁絕對網址(取自 API 的 html_url)
|
||||
# 連結一律寫成 [文字](絕對網址),網址就取自這裡,不自行組路徑
|
||||
# gitea.sh pr-create <owner>/<repo> <head> <base> <title> <body-file> # 建立 PR,印出 PR URL
|
||||
# gitea.sh pr-status <owner>/<repo> <pr-index> # 印出「{state} {merged} {mergeable}」
|
||||
# gitea.sh pr-get <owner>/<repo> <pr-index> # 印出 PR 的標題、base 分支與描述,供比對用
|
||||
@@ -243,8 +244,8 @@ sys.stdout.write(base64.b64decode(d.get("content_base64","")).decode("utf-8"))
|
||||
' ;;
|
||||
wiki-url)
|
||||
# 印出 wiki 頁的絕對網址,取自 API 回應的 html_url,不自行組路徑。
|
||||
# 跨存取庫連結(例如 LOG 頁連到 PLAN 頁,而兩者的 JSC_WIKI_REPO_{TYPE} 不同)
|
||||
# 只有絕對網址會通:[[頁名]] 與 markdown 相對連結都只在同一個 wiki 內解析。
|
||||
# 連結一律寫成 [文字](絕對網址),網址全部取自這裡:只有絕對網址在哪裡都解得到,
|
||||
# wiki 內部連結語法與 markdown 相對連結都只在同一個 wiki 內解析,跨存取庫就落到別頁去。
|
||||
or="${1:?owner/repo required}"; page="${2:?page required}"
|
||||
if ! out=$(req GET "/repos/$or/wiki/page/$page" 2>/dev/null); then
|
||||
case "$(req_code)" in
|
||||
|
||||
Executable
+231
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env sh
|
||||
# link-check.sh — 連結可達性檢查(連結寫進任何文件之前先跑這一支)。
|
||||
# 用法:
|
||||
# link-check.sh <網址>...
|
||||
# printf '%s\n' <網址>... | link-check.sh # 沒給參數就從標準輸入一行一個讀
|
||||
#
|
||||
# 輸出: 每個網址一行,三欄以 TAB 分隔
|
||||
# {OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明}
|
||||
# OK = 連得到
|
||||
# DEAD = 連不到,呼叫端不得把它寫進文件
|
||||
# SKIP = 沒有可查的端點(mailto:、錨點、相對路徑),不影響結束碼
|
||||
#
|
||||
# 驗證方式依網址種類分流:
|
||||
# Gitea wiki 頁(<GITEA_HOST>/<owner>/<repo>/wiki/<頁名>) -> API /repos/<owner>/<repo>/wiki/page/<頁名>
|
||||
# Gitea 議題(…/issues/<編號>) -> API /repos/<owner>/<repo>/issues/<編號>
|
||||
# 其他 Gitea 網址 -> HTTP HEAD,帶金鑰
|
||||
# 非 Gitea 的外部網址 -> HTTP HEAD,不帶金鑰
|
||||
#
|
||||
# 規則:
|
||||
# - Gitea 一律走 API,不看網頁狀態碼。私有存取庫的網頁網址對未登入請求一律回 404,
|
||||
# 用網頁狀態碼判斷會把好連結判成壞連結,接著整批砍掉還在的頁。
|
||||
# - 外部網址不帶金鑰。金鑰是這個站台的憑證,送去別的主機就是憑證外洩。
|
||||
# - 這支只回報,不改任何檔案。呼叫端拿到結束碼 0 才可以把連結寫進文件。
|
||||
#
|
||||
# 環境變數: GITEA_HOST(例 https://gitea.jsc.idv.tw)、GITEA_TOKEN
|
||||
# GITEA_TOKEN 未設定時退回 tea CLI 的登入金鑰(~/.config/tea/config.yml)。
|
||||
# 優先取 url 與 GITEA_HOST 同主機的登入:tea 可以同時登入多個站台,只看 default
|
||||
# 會把 A 站的金鑰送去 B 站,那是憑證外洩,不是單純取錯值。
|
||||
#
|
||||
# 結束碼: 0=全部連得到 1=至少一筆連不到 2=用法錯誤(一個網址都沒給)
|
||||
# 3=清單裡有 Gitea 網址,但 GITEA_HOST 未設定 7=Gitea 認證失敗(HTTP 401、403)
|
||||
# 7 一定要與 1 分開:金鑰失效時,Gitea 對私有存取庫的回應與「頁不存在」難以分辨。
|
||||
# 兩者混用,一次金鑰過期就把整批還在的頁判成死連結,接著這些頁會被當成壞連結刪掉或改寫。
|
||||
set -eu
|
||||
|
||||
TAB=$(printf '\t')
|
||||
|
||||
emit() { # <狀態> <網址> <說明>
|
||||
printf '%s%s%s%s%s\n' "$1" "$TAB" "$2" "$TAB" "$3"
|
||||
}
|
||||
|
||||
host_of() { # <網址> -> 只留主機名,轉小寫
|
||||
printf '%s' "$1" | sed 's#^[a-zA-Z]*://##; s#/.*##; s#^.*@##; s#:[0-9]*$##' | tr 'A-Z' 'a-z'
|
||||
}
|
||||
|
||||
path_of() { # <網址> -> 去掉協定、主機、查詢字串與錨點之後的路徑,頭尾不留斜線
|
||||
printf '%s' "$1" | sed 's#^[a-zA-Z]*://[^/]*##; s/[?#].*$//; s#^/##; s#/$##'
|
||||
}
|
||||
|
||||
seg() { # <路徑> <序號> -> 第 n 段
|
||||
printf '%s' "$1" | cut -d/ -f"$2"
|
||||
}
|
||||
|
||||
nseg() { # <路徑> -> 段數
|
||||
printf '%s' "$1" | awk -F/ '{print NF}'
|
||||
}
|
||||
|
||||
looks_like_gitea_path() { # <路徑> -> 形狀像 wiki 頁或議題就回 0
|
||||
_p="$1"
|
||||
[ "$(nseg "$_p")" -ge 4 ] || return 1
|
||||
case "$(seg "$_p" 3)" in
|
||||
wiki|issues) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_token() { # 印出這次要用的金鑰;一把都沒有就印空字串
|
||||
if [ -n "${GITEA_TOKEN:-}" ]; then printf '%s' "$GITEA_TOKEN"; return 0; fi
|
||||
cfg="${HOME:-}/.config/tea/config.yml"
|
||||
[ -f "$cfg" ] || return 0
|
||||
awk -v want="$GITEA_HOST_ONLY" '
|
||||
function flush() {
|
||||
if (tok == "") return
|
||||
if (first == "") first = tok
|
||||
if (want != "" && host == want) match_tok = tok
|
||||
if (def) def_tok = tok
|
||||
}
|
||||
/^ *- / { flush(); tok=""; host=""; def=0 }
|
||||
/^ *token: */ { line=$0; sub(/^ *token: */,"",line); gsub(/"/,"",line); tok=line }
|
||||
/^ *url: */ { line=$0; sub(/^ *url: */,"",line); gsub(/"/,"",line)
|
||||
sub(/^https?:\/\//,"",line); sub(/\/.*$/,"",line); host=line }
|
||||
/^ *default: *true/ { def=1 }
|
||||
END {
|
||||
flush()
|
||||
if (match_tok != "") { printf "%s", match_tok; exit }
|
||||
if (def_tok != "") printf "%s", def_tok
|
||||
else printf "%s", first
|
||||
}
|
||||
' "$cfg"
|
||||
}
|
||||
|
||||
http_status() { # <HEAD|GET> <網址> [金鑰] -> 印出 HTTP 狀態碼,連不上印 000
|
||||
_m="$1"; _u="$2"; _t="${3:-}"
|
||||
# HEAD 走 -I,不走 -X HEAD:後者讓 curl 等一個永遠不會來的內文,整支腳本卡在那裡。
|
||||
# GET 退讓只要第一個位元組(-r 0-0),拿的是狀態碼,不必把整份內容抓回來。
|
||||
if [ "$_m" = HEAD ]; then set -- -I; else set -- -r 0-0; fi
|
||||
# 連線與總時間分兩個上限。主機根本不通時,5 秒就收工;主機活著但慢,才讓它用滿 20 秒。
|
||||
# 只設總時間的話,一頁十條連結碰上不通的主機要等三分多鐘,呼叫端會以為整支腳本掛了。
|
||||
if [ -n "$_t" ]; then
|
||||
_c=$(curl -sS -o /dev/null -L --connect-timeout 5 --max-time 20 -w '%{http_code}' "$@" \
|
||||
-H "Authorization: token $_t" "$_u" 2>/dev/null || true)
|
||||
else
|
||||
_c=$(curl -sS -o /dev/null -L --connect-timeout 5 --max-time 20 -w '%{http_code}' "$@" "$_u" 2>/dev/null || true)
|
||||
fi
|
||||
case "$_c" in ''|*[!0-9]*) _c=000 ;; esac
|
||||
printf '%s' "$_c"
|
||||
}
|
||||
|
||||
api_status() { # <API 路徑> -> 印出 HTTP 狀態碼,連不上印 000
|
||||
_c=$(curl -sS -o /dev/null --connect-timeout 5 --max-time 20 -w '%{http_code}' \
|
||||
-H "Authorization: token $TOKEN" "$API$1" 2>/dev/null || true)
|
||||
case "$_c" in ''|*[!0-9]*) _c=000 ;; esac
|
||||
printf '%s' "$_c"
|
||||
}
|
||||
|
||||
auth_stop() { # <網址> -> 印出這一筆,回報金鑰問題並中止
|
||||
emit SKIP "$1" "Gitea 認證失敗(HTTP $2),無法判定"
|
||||
echo "[jsc][連結檢查][ERR]:Gitea 金鑰失效或權限不足(HTTP $2)。請換一支有效的 GITEA_TOKEN,或重新 tea login 之後再跑一次;在那之前不要把任何連結判成死連結。" >&2
|
||||
exit 7
|
||||
}
|
||||
|
||||
# ---- 收集網址 ----
|
||||
list=$(mktemp)
|
||||
raw=$(mktemp)
|
||||
trap 'rm -f "$list" "$raw"' EXIT
|
||||
if [ "$#" -gt 0 ]; then
|
||||
printf '%s\n' "$@" > "$raw"
|
||||
else
|
||||
cat > "$raw"
|
||||
fi
|
||||
# 先去掉行尾空白,再丟掉空白行:清單多半是別的指令產出的,尾巴常帶一行空的,
|
||||
# 那一行會被當成一個網址,最後回一個沒人看得懂的 DEAD。
|
||||
sed 's/[[:space:]]*$//' "$raw" | grep -v '^[[:space:]]*$' > "$list" || true
|
||||
if [ ! -s "$list" ]; then
|
||||
echo 'usage: link-check.sh <網址>... ;或用標準輸入一行一個網址' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ---- Gitea 站台設定 ----
|
||||
GITEA_HOST_ONLY=$(host_of "${GITEA_HOST:-}")
|
||||
if [ -z "$GITEA_HOST_ONLY" ]; then
|
||||
# GITEA_HOST 沒設定就認不出哪些是 Gitea 網址,只能看路徑形狀。認出一筆就整批停下來:
|
||||
# 少了主機設定,Gitea 連結只驗得到網頁狀態碼,私有存取庫會全部被判成死連結。
|
||||
while IFS= read -r u; do
|
||||
if looks_like_gitea_path "$(path_of "$u")"; then
|
||||
echo "[jsc][連結檢查][ERR]:清單裡有 Gitea 網址($u),但 GITEA_HOST 未設定。請先設定 GITEA_HOST 再跑一次,不得跳過驗證。" >&2
|
||||
exit 3
|
||||
fi
|
||||
done < "$list"
|
||||
fi
|
||||
|
||||
case "${GITEA_HOST:-}" in
|
||||
http://*|https://*) HOST="${GITEA_HOST:-}" ;;
|
||||
'') HOST='' ;;
|
||||
*) HOST="https://${GITEA_HOST}" ;;
|
||||
esac
|
||||
API=''
|
||||
[ -z "$HOST" ] || API="${HOST%/}/api/v1"
|
||||
TOKEN=$(resolve_token)
|
||||
|
||||
# ---- 逐筆檢查 ----
|
||||
dead=0
|
||||
while IFS= read -r url; do
|
||||
case "$url" in
|
||||
http://*|https://*) ;;
|
||||
*)
|
||||
emit SKIP "$url" "不是 http 或 https 網址,沒有可查的端點"
|
||||
continue ;;
|
||||
esac
|
||||
|
||||
uhost=$(host_of "$url")
|
||||
upath=$(path_of "$url")
|
||||
|
||||
if [ -n "$GITEA_HOST_ONLY" ] && [ "$uhost" = "$GITEA_HOST_ONLY" ]; then
|
||||
owner=$(seg "$upath" 1); repo=$(seg "$upath" 2); kind=$(seg "$upath" 3)
|
||||
rest=''
|
||||
[ "$(nseg "$upath")" -lt 4 ] || rest=$(printf '%s' "$upath" | cut -d/ -f4-)
|
||||
# 編輯畫面的網址尾巴指的還是同一頁,去掉再查。
|
||||
rest=$(printf '%s' "$rest" | sed 's#/_edit$##; s#/_new$##; s#/_pages$##')
|
||||
|
||||
if [ "$kind" = wiki ] && [ -n "$rest" ]; then
|
||||
code=$(api_status "/repos/$owner/$repo/wiki/page/$rest")
|
||||
case "$code" in
|
||||
2*) emit OK "$url" "wiki 頁存在(API HTTP $code)" ;;
|
||||
401|403) auth_stop "$url" "$code" ;;
|
||||
404) emit DEAD "$url" "wiki 頁不存在(API HTTP 404)"; dead=1 ;;
|
||||
*) emit DEAD "$url" "wiki API 回 HTTP $code"; dead=1 ;;
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$kind" = issues ] && [ -n "$rest" ]; then
|
||||
idx=$(printf '%s' "$rest" | cut -d/ -f1)
|
||||
case "$idx" in
|
||||
''|*[!0-9]*)
|
||||
emit DEAD "$url" "議題編號不是數字:$idx"; dead=1; continue ;;
|
||||
esac
|
||||
code=$(api_status "/repos/$owner/$repo/issues/$idx")
|
||||
case "$code" in
|
||||
2*) emit OK "$url" "議題存在(API HTTP $code)" ;;
|
||||
401|403) auth_stop "$url" "$code" ;;
|
||||
404) emit DEAD "$url" "議題不存在(API HTTP 404)"; dead=1 ;;
|
||||
*) emit DEAD "$url" "議題 API 回 HTTP $code"; dead=1 ;;
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
code=$(http_status HEAD "$url" "$TOKEN")
|
||||
case "$code" in
|
||||
2*|3*) emit OK "$url" "Gitea 網址可達(HEAD HTTP $code)" ;;
|
||||
401|403) auth_stop "$url" "$code" ;;
|
||||
000) emit DEAD "$url" "連不上 Gitea 主機(沒有回應)"; dead=1 ;;
|
||||
*) emit DEAD "$url" "Gitea 網址回 HTTP $code"; dead=1 ;;
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
# 外部網址:不帶金鑰。HEAD 被擋掉時再用 GET 試一次,有些站台只擋 HEAD。
|
||||
code=$(http_status HEAD "$url")
|
||||
case "$code" in
|
||||
2*|3*) ;;
|
||||
*) code=$(http_status GET "$url") ;;
|
||||
esac
|
||||
case "$code" in
|
||||
2*|3*) emit OK "$url" "外部網址可達(HTTP $code)" ;;
|
||||
000) emit DEAD "$url" "連不上主機(沒有回應)"; dead=1 ;;
|
||||
*) emit DEAD "$url" "外部網址回 HTTP $code"; dead=1 ;;
|
||||
esac
|
||||
done < "$list"
|
||||
|
||||
exit "$dead"
|
||||
Reference in New Issue
Block a user