本文档记录一次完整部署的全过程:环境探测、方案选择、每一步的实际命令与输出、
遇到的所有坑及解法、以及最终产物源码。目标是让另一个 AI(或人)拿到**任意一台
Windows 电脑**上,能照着复现出一模一样的服务。

  • 部署日期:2026-09-04
  • 本机:Windows 11 Pro 10.0.26200,主机名 masha-magicbook
  • 部署目录:D:\cygwin64\home\masha\Documents\workspace\filebrowser
  • 服务:FileBrowser v2.63.23(官方 Windows amd64 单文件二进制)
  • 监听:0.0.0.0:8080,根目录 D:\,中文界面
⚠️ 阅读顺序建议:先看 §1~§5 了解「怎么做」,再读 §8 踩坑记录。
§8 是本文档最有价值的部分——每一条都是实际撞出来的,跳过会重蹈覆辙。

1. 需求

用户的要求,逐条对应到后面的实现:

需求实现
在当前目录部署一个 file browser 服务用官方 filebrowser/filebrowser,不自己写
局域网中能用网页管理文件监听 0.0.0.0:8080
管理范围:整个 D 盘配置 --root "D:/"
开启密码登录admin 用户,--perm.admin

2. 环境探测(第一步永远是这个)

不要假设环境。先摸底,结论直接决定方案。

for c in python python3 pip pip3 node npm docker git go; do
  printf "%-8s " "$c"; command -v $c >/dev/null 2>&1 && $c --version 2>&1 | head -1 || echo "(未安装)"
done

本机实测结果:

python   Python 3.14.6
pip      pip 26.1.2
node     v24.16.0
npm      11.13.0
docker   (未安装)
git      git version 2.43.0.windows.1
go       (未安装)

结论:没有 Docker,没有 Go → 只能下载现成的预编译二进制。

2.1 拿局域网 IP

不要用 ipconfig,它的输出会被 bash 当成二进制文件:

$ ipconfig | grep -i "IPv4"
grep: (标准输入): 匹配到二进制文件

改用 socket 探测(走 UDP 8.8.8.8 只为了拿本机出口网卡 IP,不真正发包):

python -c "import socket; s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); \
  s.connect(('8.8.8.8',80)); print('本机 IP:', s.getsockname()[0]); s.close()"

本机输出:192.168.5.50

2.2 确认网络与包管理可用

timeout 15 pip download flask -d /tmp/pipcheck --no-deps -q && echo "pip 网络 OK"

2.3 查 GitHub 最新发行版

WebFetch 到 github.com 被网络策略拦了(Unable to verify if domain github.com is safe to fetch),
改用 curl 打 GitHub API——这条路是通的:

curl -sSL -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/filebrowser/filebrowser/releases/latest -o /tmp/fb_release.json

解析出资产列表(注意资源命名是 <os>-<arch>-filebrowser.<ext>):

darwin-amd64-filebrowser.tar.gz      ...
linux-amd64-filebrowser.tar.gz       ...
windows-386-filebrowser.zip          ...
windows-amd64-filebrowser.zip        ...   ← 要这个
windows-arm64-filebrowser.zip        ...
filebrowser_2.63.23_checksums.txt    ...

本机 tag_name = v2.63.23published_at = 2026-07-27T20:03:21Z
运行 ./filebrowser.exe version 确认 File Browser v2.63.23/e8a388f8


3. 方案选择

方案可行理由
Docker 跑官方镜像没装 Docker
go install github.com/...没装 Go,装工具链太重
自己写 Flask/Node 文件管理器用户明确说不要自己写
下载官方 windows-amd64 二进制FileBrowser 是单文件 Go 程序,官方 release 有 Windows 包,零依赖

FileBrowser 的特点决定了后续设计:

  • 单文件:解压即用,36 MB 一个 filebrowser.exe
  • 配置存 Bolt DB 单文件 filebrowser.db,不需要装数据库
  • 数据库文件被独占锁定 → 运行中无法改用户/配置(见 §8.7)
  • 支持 -c 配置文件(.filebrowser.{json,toml,yaml}),但多数设置只在 DB 里,
    必须用 config set
  • 默认监听 127.0.0.1:8080、root .——默认值只适合本机,局域网必须显式改

4. 下载与校验(一定要校验)

cd /tmp && rm -rf fb_dl && mkdir fb_dl && cd fb_dl
BASE="https://github.com/filebrowser/filebrowser/releases/download/v2.63.23"
curl -sSfL -o filebrowser_2.63.23_checksums.txt "$BASE/filebrowser_2.63.23_checksums.txt"
curl -sSfL --retry 3 -o windows-amd64-filebrowser.zip "$BASE/windows-amd64-filebrowser.zip"

校验(官方 checksums 文件用的是 sha256sum 格式,注意文件名前有 *):

grep -i "windows-amd64" filebrowser_2.63.23_checksums.txt > expect.txt
sha256sum windows-amd64-filebrowser.zip > actual.txt
diff <(awk '{print $1}' expect.txt) <(awk '{print $1}' actual.txt) && echo "✅ 校验通过"

本机实测:

fdb1d86dfafff8b3861867c7797ce786570013088678e03de17cfd9476c72384  windows-amd64-filebrowser.zip
fdb1d86dfafff8b3861867c7797ce786570013088678e03de17cfd9476c72384 *windows-amd64-filebrowser.zip
✅ SHA256 校验通过

Windows Python 看不到 Cygwin 的 /tmp。Cygwin 的 /tmp 实际是
D:\cygwin64\tmp,Python 会报 FileNotFoundError: [Errno 2] No such file or directory: '/tmp/...'
跨用时要 cygpath -w 转换:

ZIP=$(cygpath -w /tmp/fb_dl/windows-amd64-filebrowser.zip)   # → D:\cygwin64\tmp\...

5. 解压、初始化、创建用户

5.1 解压

本机没装 unzip,用 Python:

ZIP=$(cygpath -w /tmp/fb_dl/windows-amd64-filebrowser.zip)
DEST=$(cygpath -w "$PWD")
python -c "import zipfile; \
  [print(i.filename, i.file_size) for i in zipfile.ZipFile(r'$ZIP').infolist()]"

先列一遍内容(确认无嵌套目录、无路径穿越)再解压:

CHANGELOG.md            0.13 MB
LICENSE                 0.01 MB
README.md               0.00 MB
filebrowser.exe         35.58 MB
python -c "import zipfile; \
  [zipfile.ZipFile(r'$ZIP').extract(i, r'$DEST') for i in zipfile.ZipFile(r'$ZIP').infolist()]"
Cygwin bash 执行 .exe 要用正斜杠路径"D:\...\filebrowser.exe" 会报
未找到命令。用 /home/masha/Documents/workspace/filebrowser/filebrowser.exe
或直接 ./filebrowser.exe

5.2 初始化数据库

./filebrowser.exe config init -d filebrowser.db

输出会打印一份完整默认配置,关键默认值(这些就是要改的):

Server:
  Address:                   127.0.0.1      ← 必须改成 0.0.0.0
  Port:                      8080
  Root:                      .              ← 必须改成目标盘
  Log:                       stdout
  Token Expiration Time:     2h
  Exec Enabled:              false          ← 默认关命令执行,安全,保持
Defaults:
  Locale:                    en             ← 改成 zh-cn
  Minimum Password Length:   12

5.3 写入配置

./filebrowser.exe config set -d filebrowser.db \
  --address 0.0.0.0 \
  --port 8080 \
  --root "D:/" \
  --locale zh-cn \
  --tokenExpirationTime 12h

回读确认(config set 会自己打印结果):

Server:
  Root:                      D:/
  Address:                   0.0.0.0
  Port:                      8080
  Token Expiration Time:     12h
Defaults:
  Locale:                    zh-cn
"D:/" 而不是 "D:\"——正斜杠在 bash 里不用转义,且 Go 在 Windows 上两种都认。

5.4 创建管理员用户

PASS=$(python -c "
import secrets, string
keep = ''.join(c for c in (string.ascii_letters+string.digits) if c not in 'O0oI1l')
print(''.join(secrets.choice(keep) for _ in range(16)))
")
./filebrowser.exe users add admin "$PASS" --perm.admin -d filebrowser.db

生成密码时去掉 O 0 o I 1 l,因为用户要在手机上手工输入。
长度 ≥12(minimumPasswordLength 默认 12,短了会被拒)。

验证:

./filebrowser.exe users ls -d filebrowser.db
ID  Username  Scope  Locale  V. Mode  S.Click  Red. After C/M  Admin  ...
1   admin     /      zh-cn   list     false    false           true   ...

Scope 显示 / = 相对 root 的根,也就是 D:\。正确。

⚠️ --scope 千万不要传绝对路径。传 --scope "D:/" 会得到:

Error: failed to create user home dir: [/D:]: mkdir D:\D:: The filename,
directory name, or volume label syntax is incorrect.

root 已经是 D:/ 了,scope 必须留空(默认 .)或传相对路径。
另外注意:config set-r/--rootusers add 只有 --scope,没有 -r


6. 启动服务

6.1 怎么让进程「活下来」

这是个关键设计点。三个候选:

方式问题
前台跑 ./filebrowser.exe关掉终端就没了
nohup ... & / start /B进程仍在终端的进程组里,关窗口时收到 CTRL_CLOSE_EVENT 被一起杀掉
**`Popen(creationflags=DETACHED_PROCESS \CREATE_NEW_PROCESS_GROUP)`**✅ 完全脱离,关窗口不影响

所以启动逻辑写进 Python 脚本(见 §7),不用 .bat 直接跑 exe。

日志:FileBrowser 的 -l 支持 file,stdout 这类组合,但语法没文档。
不如直接让 stdout 追加到 filebrowser.out.log,稳。

6.2 验证服务

$ ./filebrowser.exe -d filebrowser.db     # 首次启动
2026/09/04 22:33:09 Listening on [::]:8080

netstat -ano 应看到两条(IPv4 + IPv6):

TCP    0.0.0.0:8080    0.0.0.0:0    LISTENING    <pid>
TCP    [::]:8080       [::]:0       LISTENING    <pid>

7. 控制脚本 filebrowser_ctl.py(完整源码)

设计要点:

  1. 系统工具走绝对路径——本机 PATH 里 Cygwin 的 /usr/bin 排在 system32 前面,
    timeoutnetstat 会被解析成 GNU 版。见 §8.12
  2. 端口探测用 socket,不用 netstat,彻底不依赖 PATH
  3. 输出前 reconfigure stdout 为 UTF-8,否则 Windows 控制台 GBK 编码打中文/emoji 会崩
  4. start/stop 幂等,重复执行不报错
  5. passwd 自动「停服务 → 改密码 → 启动 → 真发一次登录请求验证」
#!/usr/bin/env python3
"""FileBrowser 服务控制脚本。

用法:
    python filebrowser_ctl.py start            启动服务
    python filebrowser_ctl.py stop             停止服务
    python filebrowser_ctl.py restart          重启
    python filebrowser_ctl.py status           查看状态
    python filebrowser_ctl.py passwd [新密码]   改密码(服务会自动停/启)

说明: 这台机器的 PATH 里 Cygwin 的 /usr/bin 排在 system32 前面,
      所以系统命令都走绝对路径调用,不依赖 PATH 解析。
"""
import csv
import io
import json
import os
import secrets
import socket
import string
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

# Windows 控制台默认 GBK,输出中文/emoji 会崩
for _s in (sys.stdout, sys.stderr):
    if hasattr(_s, "reconfigure"):
        _s.reconfigure(encoding="utf-8", errors="replace")

BASE = Path(__file__).resolve().parent
EXE = BASE / "filebrowser.exe"
DB = BASE / "filebrowser.db"
LOG = BASE / "filebrowser.out.log"
CRED = BASE / "CREDENTIALS.txt"
IMAGE = "filebrowser.exe"
PORT = 8080

# Cygwin 传进来的 PATH 对原生 Windows 程序不可用,系统工具一律走绝对路径
_SYSTEMROOT = Path(os.environ.get("SystemRoot") or os.environ.get("WINDIR") or r"C:\Windows")
TASKLIST = _SYSTEMROOT / "System32" / "tasklist.exe"
TASKKILL = _SYSTEMROOT / "System32" / "taskkill.exe"

DETACHED_PROCESS = 0x00000008
CREATE_NEW_PROCESS_GROUP = 0x00000200


# ---------------------------------------------------------------- 基础工具
def _run(cmd, **kw):
    return subprocess.run([str(c) for c in cmd], capture_output=True, text=True, **kw)


def pids() -> list[int]:
    """按进程名查 PID。"""
    r = _run([TASKLIST, "/FI", f"IMAGENAME eq {IMAGE}", "/FO", "CSV", "/NH"])
    found = []
    for row in csv.reader(io.StringIO(r.stdout)):
        if row and row[0].strip('"').lower() == IMAGE.lower():
            try:
                found.append(int(row[1]))
            except (IndexError, ValueError):
                pass
    return found


def port_open(port: int = PORT) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(1)
        return s.connect_ex(("127.0.0.1", port)) == 0


def lan_ip() -> str:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except OSError:
        return "本机IP"


def urls() -> str:
    return f"http://localhost:{PORT} / http://{lan_ip()}:{PORT}"


def wait_port(port: int, timeout: float, *, expect: bool) -> bool:
    deadline = time.time() + timeout
    while time.time() < deadline:
        if port_open(port) == expect:
            return True
        time.sleep(0.3)
    return port_open(port) == expect


# ---------------------------------------------------------------- 子命令
def start() -> int:
    if port_open():
        print(f"FileBrowser 已在运行 (PID {pids() or '?'}), 端口 {PORT}")
        return 0

    if not EXE.exists():
        sys.exit(f"找不到 {EXE}")
    if not DB.exists():
        sys.exit(f"找不到数据库 {DB}, 请先初始化配置")

    with LOG.open("a", encoding="utf-8", errors="replace") as logf:
        subprocess.Popen(
            [str(EXE), "-d", str(DB)],
            cwd=str(BASE),
            stdin=subprocess.DEVNULL,
            stdout=logf,
            stderr=subprocess.STDOUT,
            creationflags=DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
        )

    if not wait_port(PORT, timeout=15, expect=True):
        print(f"启动超时, 请看日志 {LOG.name}:")
        print(LOG.read_text(encoding="utf-8", errors="replace")[-2000:])
        return 1

    print(f"FileBrowser 已启动  PID {pids()}")
    print(f"  本机访问:    http://localhost:{PORT}")
    print(f"  局域网访问:  http://{lan_ip()}:{PORT}")
    return 0


def stop() -> int:
    ids = pids()
    if not ids:
        print("FileBrowser 本来就没在运行")
        return 0

    for pid in ids:
        r = _run([TASKKILL, "/F", "/PID", str(pid)])
        print(f"  终止 PID {pid}: {'成功' if r.returncode == 0 else '失败'}")

    if not wait_port(PORT, timeout=10, expect=False):
        print("端口还没释放, 请重试")
        return 1
    print(f"FileBrowser 已停止 (共 {len(ids)} 个进程)")
    return 0


def restart() -> int:
    stop()
    return start()


def status() -> int:
    ids = pids()
    listening = port_open()
    print(f"进程:      {ids or '无'}")
    print(f"端口 {PORT}: {'LISTENING' if listening else '未监听'}")
    if listening:
        print(f"访问地址:  {urls()}")
        if CRED.exists():
            print(f"用户名:    {CRED.read_text().splitlines()[0]}")
    return 0 if listening else 1


# ---------------------------------------------------------------- 密码
def _generate(length: int = 16) -> str:
    # 去掉易混淆字符,方便在外接设备上手工输入
    alphabet = "".join(
        c for c in (string.ascii_letters + string.digits) if c not in "O0oI1l"
    )
    return "".join(secrets.choice(alphabet) for _ in range(length))


def verify_login(username: str, password: str) -> str:
    """返回 ok / bad / offline。"""
    body = json.dumps({"username": username, "password": password}).encode()
    req = urllib.request.Request(
        f"http://127.0.0.1:{PORT}/api/login",
        data=body,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            return "ok" if resp.status == 200 else "bad"
    except urllib.error.HTTPError as e:
        print(f"    登录被拒: HTTP {e.code}")
        return "bad"
    except Exception as e:
        print(f"    服务未响应: {e}")
        return "offline"


def passwd(new_password: str | None = None) -> int:
    """改密码需要独占数据库文件, 所以先停服务再启动。"""
    username = "admin"
    password = new_password or _generate()

    if len(password) < 12:
        sys.exit("FileBrowser 要求密码至少 12 位")

    was_running = bool(pids())
    if was_running:
        print("==> 停止服务以解锁数据库")
        stop()
        time.sleep(1)

    r = _run([EXE, "users", "update", username, "--password", password, "-d", str(DB)])
    if r.returncode != 0:
        sys.exit(f"修改密码失败:\n{r.stderr}{r.stdout}")
    CRED.write_bytes(f"{username}\n{password}\n".encode())

    print(f"==> 密码已更新 ({len(password)} 位)")
    if was_running:
        print("==> 重新启动服务")
        start()
        time.sleep(1)

    result = verify_login(username, password)
    msg = {
        "ok": "✅ 登录验证通过",
        "offline": "⚠️  服务未运行, 已改密码但未验证",
        "bad": "❌ 登录验证失败",
    }[result]
    print(msg)
    print()
    print(f"用户名: {username}")
    print(f"密  码: {password}")
    print(f"凭据已写入 {CRED.name} (确认无误后可删除该文件)")
    return 0 if result != "bad" else 1


# ---------------------------------------------------------------- 入口
def main() -> int:
    cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
    handlers = {
        "start": start,
        "stop": stop,
        "restart": restart,
        "status": status,
        "passwd": lambda: passwd(sys.argv[2] if len(sys.argv) > 2 else None),
    }
    if cmd not in handlers:
        sys.exit(__doc__.strip())
    return handlers[cmd]()


if __name__ == "__main__":
    raise SystemExit(main())

8. .bat 双击包装

只有一行逻辑,作用是「进目录 + 调 Python」:

@echo off
cd /d "%~dp0"
"C:\Users\masha\AppData\Local\Python\bin\python.exe" filebrowser_ctl.py start %*

start.bat / stop.bat / restart.bat / status.bat / passwd.bat 只有最后一个参数不同。

⚠️ .bat 必须存成 GBK + CRLF

这是本次最坑的地方,不要用编辑器/Write 工具直接存 UTF-8。用 Python 生成:

import pathlib, os

PY = r"C:\Users\masha\AppData\Local\Python\bin\python.exe"

def gbk(name, lines):
    """lines 是 str 列表, 写成 GBK + CRLF 的批处理。"""
    data = ('\r\n'.join(lines) + '\r\n').encode('gbk')
    pathlib.Path(name).write_bytes(data)

for name, cmd in [('start.bat','start'), ('stop.bat','stop'),
                  ('restart.bat','restart'), ('status.bat','status'),
                  ('passwd.bat','passwd')]:
    gbk(name, [
        '@echo off',
        'cd /d "%~dp0"',
        f'"{PY}" filebrowser_ctl.py {cmd} %*',
    ])

三个条件缺一不可,缺一就会解析错乱(症状见 §8.10~8.11):

  • GBK 编码(本机控制台代码页是 936)
  • CRLF 行尾
  • 无 BOM

9. 防火墙

netsh advfirewall firewall add rule name="FileBrowser (TCP 8080)" \
  dir=in action=allow protocol=TCP localport=8080

需要管理员权限,普通会话执行会返回:

请求的操作需要提升(作为管理员运行)。

所以单独做了 install-firewall-rule.bat,用户右键「以管理员身份运行」。
脚本里用 net session >nul 2>&1 做提权自检(这是经典的 Windows 管理员检测手法)。

@echo off
cd /d "%~dp0"

net session >nul 2>&1
if %errorlevel% neq 0 (
    echo [错误] 本脚本需要管理员权限
    echo        请关闭此窗口, 右键本文件, 选择 以管理员身份运行
    pause
    exit /b 1
)

netsh advfirewall firewall delete rule name="FileBrowser (TCP 8080)" >nul 2>&1
netsh advfirewall firewall add rule name="FileBrowser (TCP 8080)" dir=in action=allow protocol=TCP localport=8080
if %errorlevel% equ 0 (
    echo [成功] 防火墙规则已添加, 局域网内可访问:
    echo        http://192.168.5.50:8080
    echo.
    echo 提示: 这是允许所有设备入站的宽规则
    echo       若要限制为特定网段, 请到 高级安全 Windows 防火墙 中调整
) else (
    echo [失败] 无法添加规则
)
pause

规则不存在时的查证方式:

netsh advfirewall firewall show rule name="FileBrowser (TCP 8080)" verbose

10. 验证清单(照这个顺序验,每步都有预期值)

1) 正确密码登录        -> HTTP 200,返回 JWT 字符串
2) 错误密码登录        -> HTTP 403          (防冒用)
3) 无 token 访问资源   -> HTTP 401          (认证生效)
4) GET /api/resources/. -> HTTP 200,23 个条目(D 盘根目录)
5) POST /api/resources/<新目录> -> HTTP 200
   POST /api/resources/<dir>/f.txt -> HTTP 200
   GET  读回                        -> HTTP 200,内容一致
6) DELETE              -> HTTP 204
7) 磁盘上确认目录已消失(`find / -maxdepth 1 -name "__fb_selftest*"` 应为空)

要点:

  • DELETE 返回 204 后再 GET 可能返回 200 而不是 404——这是 API 的兜底行为,
    不代表删除失败。必须回磁盘确认,否则会把假阳性当通过。
  • 本机 curl -I-o /tmp/... 会报 curl: (23) client returned ERROR on write
    这是 Cygwin curl 的无害怪癖,HTTP 状态码仍然返回。别被它吓到,也别用它当失败信号。
  • 用 Python urllib 做验证最干净。注意 urlopen 对 4xx 会抛 HTTPError
    except 住才能断言"应该失败"的场景。

11. 踩坑记录(按发现顺序,全部实撞)

11.1 Cygwin 挂载点搞错

写了 /cygwin64/home/masha/...,偶尔成功偶尔 No such file or directory
真相:/home/masha 才是挂载点(映射 D:\cygwin64\home\masha),
/cygwin64/... 不是挂载点,靠偶发的路径自动转换才偶尔能用。
解法:pwd 看清楚,用 /home/masha/...

11.2 并行 Bash 调用冲突

一次发两个 Bash 调用,其中一个的 cd 莫名失败。
解法:这台机器上一次只发一个 Bash 调用

11.3 Cygwin bash 跑 .exe 要正斜杠

"D:\...\filebrowser.exe" version未找到命令
解法:用 ./filebrowser.exe/home/masha/.../filebrowser.exe

11.4 Windows Python 看不到 /tmp

FileNotFoundError: '/tmp/fb_dl/...'
解法:cygpath -w 转成 Windows 路径再交给 Python。

11.5 Windows 控制台 GBK 编码

Python 打印中文/emoji 直接 UnicodeEncodeError: 'gbk' codec can't encode character '✅'
解法:脚本开头

for _s in (sys.stdout, sys.stderr):
    if hasattr(_s, "reconfigure"):
        _s.reconfigure(encoding="utf-8", errors="replace")

11.6 users add --scope 传绝对路径

--scope "D:/"Error: failed to create user home dir: [/D:]: mkdir D:\D:
解法:scope 留空(默认 .),它相对 root 解析。

11.7 数据库被独占锁定

服务运行时执行 users updateError: timeout
Bolt DB 是文件锁,运行中不可写。
解法:先停服务再改用户/配置passwd 子命令已自动处理。

11.8 taskkill 参数

taskkill //IM filebrowser.exe //F(Cygwin 双斜杠惯例)→ 无效参数/选项 - '//IM'
解法:单斜杠 taskkill /IM ... /F,或干脆用 PowerShell Stop-Process -Name filebrowser -Force

11.9 Cygwin 写文件偷偷加 CRLF

$ printf 'admin\n%s\n' "$PASS" > CREDENTIALS.txt
$ xxd CREDENTIALS.txt
41 64 6d 69 6e 0a 5a 76 7a 73 51 34 5a 55 36 4b 4b 56 66 6e 70 79 0d 0a
                                      ^^

$(sed -n '2p' CREDENTIALS.txt) 取到的密码尾部带 \r(命令替换只剥 \n 不剥 \r)。
更阴险的是:\r 在 Python 源码里是行边界,塞进单引号字符串里会得到
SyntaxError: unterminated string literal
后果:当时用被污染的密码建了用户,导致登录一律 403,还以为是别的问题。
解法:凭据文件用 Path.write_bytes() 写,读取也用 Python 而非 sed

11.10 .bat 存 UTF-8 → 解析器直接崩

中文字节被按 GBK 误读,连 ASCII 行都被拆碎:

'p0"' 不是内部或外部命令,也不是可运行的程序
'equ' 不是内部或外部命令
'.*LISTENING" >nul' 不是内部或外部命令
'-ano' 不是内部或外部命令

%~dp0if %errorlevel% equ 0netstat -ano 全被拆散)

解法:存 GBK。

11.11 .bat 用 LF 行尾 → 同样崩

修了编码之后仍然崩。原因是转码时用了 Path.read_text()——
它启用通用换行模式,把 CRLF 转成 LF,再 encode('gbk') 写回去就丢了行尾。
cmd.exe 要求 CRLF,LF 行尾会导致解析错乱。
解法:显式 '\r\n'.join(lines) + '\r\n',或 read_bytes() 处理。

顺带:chcp 65001 写在 .bat没用。cmd 按初始代码页读文件,
chcp 只影响后续 echo 的显示,救不了已经被误读的字节。

11.12 PATH 被 Cygwin 污染

/usr/local/bin
/usr/bin              ← Cygwin 在前
/c/Windows/system32   ← system32 在后

cmd.exe 继承这个 PATH,于是:

  • timeout /t 2 >nultimeout: 无效的时间间隔"/t"(调到了 GNU coreutils 版)
  • 原生 Windows 工具可能被解析到 Cygwin 版

解法:所有系统工具走绝对路径,用 os.environ["SystemRoot"]/System32/xxx.exe
端口探测改用 Python socket,不碰 netstat

另外:Windows Python 进程继承的也是这套 Cygwin 风格 PATH,shutil.which() 同样不可靠。

11.13 %~$PATH: 只能在 FOR/CALL 里用

echo timeout = %~$PATH:timeout非法的批处理替换...
(诊断思路错,与部署无关,记下来免得再试。)

11.14 WebFetch 到 github.com 被拦

Unable to verify if domain github.com is safe to fetch
解法:curlhttps://api.github.com/...,这条路通。

11.15 ipconfig 输出被当二进制

grep: (标准输入): 匹配到二进制文件
解法:用 Python socket 拿 IP。


12. 最终产物

filebrowser.exe           官方 Windows amd64 二进制 (36 MB)
filebrowser.db            配置 + 用户 (Bolt DB 单文件, 64 KB)
CREDENTIALS.txt           初始账号密码, 登录后可删
filebrowser.out.log       运行日志 (追加写入)
filebrowser_ctl.py        启停/状态/改密码 控制脚本
start/stop/restart/status/passwd.bat    双击包装 (GBK+CRLF)
install-firewall-rule.bat 防火墙放行, 需管理员
README.md                 日常使用说明
DEPLOYMENT.md             本文档

本机最终状态:

进程:      [26616]
端口 8080: LISTENING
访问地址:  http://localhost:8080 / http://192.168.5.50:8080
用户名:    admin

13. 附录:FileBrowser 关键知识点

13.1 配置优先级

Flags > 环境变量 (FB_ 前缀, 如 FB_DISABLEPREVIEWRESIZE)
      > 配置文件 (.filebrowser.{json,toml,yaml}, 查 ./、$HOME/、/etc/filebrowser/)
      > 数据库值
      > 默认值

注意:只有部分选项能放配置文件,其余只存在数据库里,
必须用 config set / config import 写。

13.2 数据库自动初始化

如果 -d 指定的数据库文件不存在,FileBrowser 进入 quick setup mode
自动建库,并用 --username / --password 两个 flag 创建首个用户。
所以最简启动是:

./filebrowser.exe -a 0.0.0.0 -p 8080 -r "D:/" -d filebrowser.db \
  --username admin --password 你的密码

本文档选了显式 config init + config set + users add,因为更好控制、可审计。

13.3 常用命令

./filebrowser.exe config init      # 初始化数据库
./filebrowser.exe config set       # 改配置
./filebrowser.exe config cat       # 打印当前配置
./filebrowser.exe config export    # 导出配置
./filebrowser.exe config import    # 导入配置
./filebrowser.exe users add        # 加用户
./filebrowser.exe users update     # 改用户
./filebrowser.exe users ls         # 列用户
./filebrowser.exe users find       # 查用户
./filebrowser.exe users rm         # 删用户
./filebrowser.exe version

全局 flag:-c/--config-d/--database(默认 ./filebrowser.db)。

13.4 权限位

--perm.admin--perm.login--perm.new--perm.rename--perm.modify
--perm.delete--perm.share--perm.download--perm.upload
--perm.archive--perm.execute--perm.pick--perm.properties

默认 create/delete/rename/modify/share/download/execute 都是开的。

13.5 API 速查

POST /api/login     body: {"username","password"}   -> 返回 JWT 字符串
X-Auth: <jwt>                        后续请求的认证头
GET  /api/resources/<path>           列目录 (返回 items 数组)
POST /api/resources/<path>           建目录
DELETE /api/resources/<path>         删除

GET /api/resources/. 返回的是对象不是数组,条目在 items 字段里:

{"items": [...], "numDirs": 23, "numFiles": 0, "sorting": {...},
 "path": ".", "name": "", "isDir": true, "type": "directory"}

遍历成 for it in data: 会遍历到键名字符串,报
TypeError: string indices must be integers, not 'str'。要取 data["items"]

13.6 其他有用默认值

  • minimumPasswordLength = 12(短了直接被拒)
  • --disableExec = true(命令执行器默认关,保持关闭更安全
  • TUS 上传分块 10 MB,重试 5 次
  • tokenExpirationTime 默认 2h
  • disableTypeDetectionByHeader = false(会读文件头判断类型)
  • 支持 -l file,stdout 这类日志组合

14. 安全注意事项(务必告知用户)

  1. 官方项目 2026-09-01 已归档,不再发版、不再修安全漏洞。启动时会打印:

    NOTICE: File Browser is being wound down.
    NOTICE: The project is archived on 2026-09-01, after which there will be no
    NOTICE: further releases and no security fixes. Known unfixed issues are at
    NOTICE: https://github.com/filebrowser/filebrowser/security/advisories

    可信家用网络内使用没问题,绝不暴露公网

  2. HTTP 明文,不是 HTTPS。配置里有 --cert/--key 可上 TLS。
  3. CREDENTIALS.txt 是明文密码。数据库里存的是 bcrypt hash,只有这个文件是明文。
    登录验证通过后就该删。
  4. 根目录给大了权限D:\ + admin 权限,网页里能看到 $RECYCLE.BIN
    System Volume Information。误删可从回收站还原,但操作要留意。
  5. 防火墙规则是"放行所有入站 8080"的宽规则。
    要限网段请到「高级安全 Windows 防火墙」里改。
  6. 想开机自启可用计划任务(需管理员):

    schtasks /Create /TN "FileBrowser" /SC ONLOGON ^
      /TR "C:\Users\masha\AppData\Local\Python\bin\python.exe D:\cygwin64\home\masha\Documents\workspace\filebrowser\filebrowser_ctl.py start" /F

    删除:schtasks /Delete /TN "FileBrowser" /F


15. 从零复现(浓缩版)

给别人看可以直接跳到这一节:

# 0. 装 Python(>=3.10); 确认 curl 可用; 查本机架构 x64/ARM64

# 1. 下载 + 校验
mkdir fb && cd fb
V=v2.63.23
B=https://github.com/filebrowser/filebrowser/releases/download/$V
curl -sSfL -o $V-checksums.txt $B/filebrowser_${V#v}_checksums.txt
curl -sSfL -o win.zip $B/windows-amd64-filebrowser.zip
sha256sum win.zip   # 比对 checksums.txt 里的 fdb1d86dfafff8b3861867c7797ce786570013088678e03de17cfd9476c72384

# 2. 解压
python -c "import zipfile; zipfile.ZipFile('win.zip').extractall('.')"

# 3. 初始化 + 配置(把 D:/ 换成你要的根目录)
./filebrowser.exe config init -d filebrowser.db
./filebrowser.exe config set -d filebrowser.db \
  -a 0.0.0.0 -p 8080 -r "D:/" --locale zh-cn --tokenExpirationTime 12h

# 4. 建用户(scope 千万别传绝对路径!)
./filebrowser.exe users add admin 你的至少12位密码 --perm.admin -d filebrowser.db

# 5. 放行防火墙(需管理员)
netsh advfirewall firewall add rule name="FileBrowser (TCP 8080)" \
  dir=in action=allow protocol=TCP localport=8080

# 6. 启动 & 验证
./filebrowser.exe -d filebrowser.db
# 浏览器打开 http://localhost:8080 和 http://<局域网IP>:8080

生产用法请补上 §7 的 filebrowser_ctl.py(它解决「关掉终端进程就死」和
「改密码要独占数据库」这两个真实问题),以及 §8 的 .bat 包装。

然后务必读一遍 §11 踩坑记录。

标签: none

添加新评论