1. 后台点鼠标和敲 occ,到底该在什么时候选后者
给 Nextcloud 加人这件事,只要账号数不超过十个,网页后台的管理面板确实够用——输入框点一点,姓名、邮箱、初始密码、所属组,一路填下来。但当 HR 丢过来一张三百行的人事表,或者要给一个部门一次性开五十个临时账号时,鼠标点法就变成了体力活,而且每一步都可能因为手抖点错组。这时候occ命令行就成了唯一现实的选项。occ是 Nextcloud 自带的控制台入口,本质是一个跑在服务端的 PHP 脚本,它直接调用 Nextcloud 的应用层接口,绕开了 HTTP 请求这一层。这意味着两件事:它不受浏览器会话超时影响,也不会因为 PHP-FPM 的max_execution_time半路掐断你的批量操作。这篇内容就把「Nextcloud 通过命令添加删除用户」这件事从头到尾拆开讲,再加上一份我自己在用的批量创建用户脚本,包含输入格式约定、幂等处理、密码生成、失败日志和一整套验证手段。
适合谁看:已经在跑 Nextcloud、能 SSH 上服务器、会对配置文件做备份的运维或者自建服务爱好者。如果你连 Nextcloud 的安装目录在哪都还不清楚,建议先在本地 Ubuntu 环境或者 WSL2 里装一个实例练手,那台机器上你怎么折腾都行,练熟了再把脚本搬到正式环境。
1.1 occ 的真实身份,以及为什么不能随便用 root 敲
Nextcloud 安装完成后,根目录下会有一个名为occ的文件,权限通常是755,属主是 web 服务用户。它不是什么独立的二进制程序,而是一个 PHP 脚本,第一行是#!/usr/bin/env php。所以你能看到的两种执行方式其实是一回事:./occ user:list和php occ user:list。我在生产环境里更推荐后面这种写法,理由很朴素——多 PHP 版本的机器上,/usr/bin/php未必是指向 FPM 用的那个版本。你可以用php -v看一眼 CLI 版本,再对照ps aux | grep php-fpm里的路径确认,两边大版本不一致时,occ加载的扩展模块可能和运行中的实例不一样,会冒出一些莫名其妙的错误。
更要紧的是执行身份。Nextcloud 在运行时会产生大量文件:data/下的用户数据、data/appdata_xxx/下的缓存与预览图、data/nextcloud.log日志。这些文件的属主必须是 web 服务用户。如果你图省事直接sudo php occ user:add xxx,新建的用户目录、写入的日志文件就会是 root 属主,web 进程随后无法读写,用户一登录就看到 500。这个坑我在早期踩过,当时排查了半天才发现是权限问题。正确的姿势是切换到 web 用户身份执行:
# Debian / Ubuntu 系 sudo -u www-data php /var/www/nextcloud/occ user:list # RHEL / CentOS / 麒麟系,web 用户通常是 apache 或 nginx sudo -u apache php /var/www/nextcloud/occ user:list如果你不确定当前实例的 web 用户是谁,ps aux | grep -E 'nginx|apache|php-fpm' | head看一眼进程属主就有了答案。
1.2 三条用户管理通道的取舍
Nextcloud 实际上给了三条管用户的路径,搞清楚它们各自适合什么场景,比背命令重要得多。
| 通道 | 入口 | 适合规模 | 主要短板 |
|---|---|---|---|
| 网页管理后台 | 设置 → 用户 | 几十人以内 | 无批量能力,纯手工 |
| occ 命令行 | 服务器 SSH | 几十到几百人 | 每次调用都要 bootstrap,逐个慢 |
| OCS Provisioning API | HTTP 接口 | 几百到上万人 | 需管理员账号与应用密码,脚本要处理网络 |
occ的单次调用启动时间在几百毫秒到两秒之间浮动,取决于实例装了多少应用、数据库响应快不快。这个开销是「每次都要重新加载整个框架」造成的,跟操作本身没关系。所以建十个用户和建一百个用户,耗时基本是线性增长。这也解释了为什么上千人的导入场景,社区里更常见的做法是走 OCS API 加并发,而不是无脑循环occ。不过对绝大多数自建实例来说,几百人的规模用occ循环完全够用,一小时内跑完,而且不需要额外开网络接口、不用管理应用密码,安全性反而更好。
2. 单账号增删改:occ user:add / user:delete 的参数与真实行为
先把单个账号的操作弄扎实,批量脚本无非是把这些动作包进循环里加异常处理。很多人写批量脚本出问题,根子在于对单条命令的行为边界没摸清。
2.1 user:add 里那几个必须知道的参数
occ user:add的完整形态是这样:
sudo -u www-data php occ user:add \ --password-from-env \ --display-name "张伟" \ --email "zhangwei@example.com" \ --group "staff" \ --group "sales" \ zhangwei这里有几个必须解释清楚的取舍。--password-from-env表示密码从环境变量OC_PASS读取,而不是从交互式提示输入。如果省略这个参数,命令会尝试在终端上提示你输入密码——在非交互环境(比如 cron 或者脚本管道)里,它会直接失败退不出去。所以批量脚本里这个参数是必须项。使用时的写法是:
sudo -u www-data env OC_PASS='一个临时密码' php occ user:add --password-from-env zhangwei为什么用env前缀而不是直接把变量写进命令行?因为sudo默认开启env_reset,会清空调用者的环境变量,sudo OC_PASS=xxx php occ ...这种写法在某些 sudoers 配置下变量会被丢掉。用env显式设置最保险。
顺带说一句安全性:通过环境变量传密码,比写成命令行参数(-p xxx那种)安全,因为命令行参数会出现在ps aux的输出里,同机器上任何用户都能看到。环境变量则只有 root 或者同 UID 的进程能读/proc/<pid>/environ。但这也不是绝对安全,所以别把密码长期放在脚本文件里硬编码。
--group参数可以重复多次,用户会被加进多个组。如果指定的组不存在,occ user:add会自动创建它,不需要提前occ group:add。不过我在脚本里还是习惯先显式建组,原因后面讲。
--display-name支持中文,但要注意服务器的 locale。如果系统 locale 是POSIX或C,PHP 拼接字符串时可能出问题。稳妥的做法是在脚本里显式设置LANG=C.UTF-8,或者在env里一起传进去。
2.2 改配额、改语言、改显示名:user:setting 的用法
账号建完之后的调整,都归occ user:setting管。它的签名是user:setting <uid> <app> <key> [value],不带 value 时就读取。
# 查询某人当前所有设置 sudo -u www-data php occ user:setting zhangwei # 查某个 app 下的设置 sudo -u www-data php occ occ user:setting zhangwei files # 设置配额为 10GB sudo -u www-data php occ user:setting zhangwei files quota "10 GB" # 把界面语言改成简体中文 sudo -u www-data php occ user:setting zhangwei core lang zh_CN # 设置时区 sudo -u www-data php occ user:setting zhangwei core timezone Asia/Shanghai配额那个参数值可以带单位,"10 GB"、"500 MB"都能识别,也可以直接写字节数。想知道有哪些可用的 key,最省事的办法是拿一个测试账号跑一遍不带 value 的查询,输出里会列出当前已设置的全部键值对。不同版本、不同应用注册的 key 名字会变,与其死记,不如现场查。
这里有个我发现很多人忽略的点:user:setting的写入是绕过网页端校验的。比如你写一个明显不合规的值,它可能照单全收,然后在用户下次登录时才暴露问题。所以脚本里批量设置完,最好抽一两个账号登录验证一下。
2.3 user:delete 到底删了什么,没删什么
occ user:delete <uid>的执行链路是:禁用账号 → 触发各应用的清理钩子 → 删除用户元数据 → 删除data/<uid>/目录。也就是说这个操作是不可逆的,文件、日历、联系人、分享记录、评论、版本历史,全都跟着走。
不同大版本在这个行为上有细微差别,有的版本会在删除前要求确认,有的会直接执行。用-n全局参数可以跳过所有交互提示:
sudo -u www-data php occ -n user:delete zhangwei-n是 Symfony Console 的全局选项,写在occ后面、子命令前面,表示「不要问我任何问题,按默认值来」。批量脚本里这个必须加,否则一旦遇到需要确认的场景,脚本就会卡在等待输入上,而你在终端前面看着它一动不动,以为在跑。
对于有大量文件的用户,user:delete可能跑很久,因为它要遍历并删除整个数据目录。如果这个用户有几十万个文件,删除过程可能触发 PHP 超时。这种情况可以在命令前加内存和超时放宽:
sudo -u www-data php -d memory_limit=1024M -d max_execution_time=0 /var/www/nextcloud/occ -n user:delete zhangwei2.4 先禁用再删除,给操作留一条后悔的路
我自己执行的流程从来不是「确定要删就直接删」。中间一定加一道user:disable:
sudo -u www-data php occ user:disable zhangwei # 观察一到两周,确认没人反馈业务中断 sudo -u www-data php occ user:delete zhangwei禁用之后账号无法登录,但数据完整保留在data/<uid>/下,随时可以occ user:enable恢复。这一两周的缓冲期救过我一次:某位离职同事的账号里存着几个外部协作用户还在引用的共享链接,禁用第三天就有人来问,直接恢复账号、把共享转移给接任者,再删。如果没有这道缓冲,就只能从备份里捞了。
禁用还有一个附带好处:它能把「这个账号是否真的没人用」这个问题暴露出来。你可以看occ user:lastseen <uid>的输出,结合禁用期间是否有报障,判断清不清得干净。
3. 把 CSV 喂给 occ:一份可落地的批量建号脚本
单条命令会了,下面进入正题。脚本我按「输入约定 → 骨架 → 幂等 → 并发边界」的顺序讲,代码可以直接抄。
3.1 输入格式为什么不用逗号分隔
绝大多数人写批量脚本第一反应是用 CSV,逗号分隔。我一开始也这么干,直到有一次显示名里带了英文逗号,整行字段全部错位,二十几个账号的显示名和邮箱串了位,清理起来比重建还麻烦。
所以我现在的约定是竖线分隔,文件叫users.psv:
uid|displayname|email|group|quota|password zhangwei|张伟|zhangwei@example.com|staff|10 GB| liting|李婷|liting@example.com|sales|5 GB| wangqiang|王强|wangqiang@example.com|sales||字段含义:用户名、显示名、邮箱、主组、配额、初始密码。最后一列留空表示让脚本自动生成随机密码。用户名建议只用小写字母、数字和下划线,不要用中文、空格和点号——虽然 Nextcloud 在多数场景下能接受,但用户名会出现在 URL、目录名和日志里,一旦有特殊字符,后续排查问题是自找麻烦。
配额那列如果留空,账号就继承系统的默认配额,通常在管理后台的「文件」设置里配置,这个行为是合理的,别在脚本里硬写一个默认值把它覆盖掉。
3.2 脚本骨架逐段说明
完整脚本如下,我按段落解释设计意图。
#!/usr/bin/env bash # # nc_bulk_user_add.sh - 从竖线分隔文件批量创建 Nextcloud 用户 # 用法: sudo -u www-data ./nc_bulk_user_add.sh users.psv # set -uo pipefail OCC="/var/www/nextcloud/occ" PHPBIN="php" CSV="${1:-}" TS="$(date +%Y%m%d_%H%M%S)" LOG="./nc_add_${TS}.log" RESULT="./nc_add_${TS}_result.psv" if [[ -z "$CSV" || ! -f "$CSV" ]]; then echo "用法: $0 <users.psv>" >&2 exit 1 fi # 结果文件含明文初始密码,收紧权限 umask 077 log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG" } occ_run() { "$PHPBIN" "$OCC" -n "$@" 2>>"$LOG" } user_exists() { occ_run user:info "$1" >/dev/null 2>&1 } gen_pass() { local raw raw="$(openssl rand -base64 18 | tr -dc 'A-Za-z0-9' | cut -c1-12)" printf '%s' "${raw}Aa1" } trim() { local s="$1" s="${s#"${s%%[![:space:]]*}"}" s="${s%"${s##*[![:space:]]}"}" printf '%s' "$s" } created=0 skipped=0 failed=0几个细节值得展开。set -uo pipefail里我刻意没加-e。原因很实际:批量任务中单条失败不应该让整个脚本中断,而是记录下来继续跑。-e会在任何非零返回码处退出,处理起来反而要到处写|| true,不如不要。
occ_run里把 stderr 追加到日志,stdout 交给调用方处理。这样user:list --output=json这类需要解析输出的调用不会被日志污染。
gen_pass用openssl rand -base64 18生成 24 个字符左右的随机串,过滤掉非字母数字,取前 12 位。这里为什么不用更常见的tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 12?因为那是个无限流管道,head取够字符就关闭管道,上游的tr会收到 SIGPIPE 退出码 141,而脚本开了pipefail,整个命令替换的退出码就变成 141,在某些上下文里会被判定为失败。openssl rand输出的是有限长度,管道能正常结束,没有这个问题。这个坑我在另一个脚本里被坑过一次,排查了半小时。
密码规则里额外拼上Aa1是为了满足密码策略应用的最低复杂度要求(通常要求同时含大小写和数字)。这个做法不算优雅,但胜在可靠。
3.3 主循环与幂等处理
while IFS='|' read -r uid dname email group quota pw; do uid="$(trim "${uid:-}")" dname="$(trim "${dname:-}")" email="$(trim "${email:-}")" group="$(trim "${group:-}")" quota="$(trim "${quota:-}")" pw="$(trim "${pw:-}")" # 跳过空行和注释行 [[ -z "$uid" || "$uid" == \#* ]] && continue if user_exists "$uid"; then log "SKIP $uid 已存在,跳过" skipped=$((skipped + 1)) printf '%s|%s|%s\n' "$uid" "" "skipped" >>"$RESULT" continue fi # 显式建组,避免并发下多个进程同时创建同一个组 if [[ -n "$group" ]]; then if ! occ_run group:add "$group" >/dev/null; then log "WARN 组 $group 创建失败或已存在,继续" fi fi [[ -z "$pw" ]] && pw="$(gen_pass)" args=(user:add --password-from-env) [[ -n "$dname" ]] && args+=(--display-name "$dname") [[ -n "$email" ]] && args+=(--email "$email") [[ -n "$group" ]] && args+=(--group "$group") args+=("$uid") if env OC_PASS="$pw" LANG=C.UTF-8 occ_run "${args[@]}"; then if [[ -n "$quota" ]]; then occ_run user:setting "$uid" files quota "$quota" >/dev/null \ || log "WARN $uid 配额设置失败:$quota" fi log "OK $uid 创建成功" created=$((created + 1)) printf '%s|%s|%s\n' "$uid" "$pw" "ok" >>"$RESULT" else log "FAIL $uid 创建失败,详见日志" failed=$((failed + 1)) printf '%s|%s|%s\n' "$uid" "" "failed" >>"$RESULT" fi done < <(sed '1s/^\xEF\xBB\xBF//' "$CSV" | tr -d '\r') log "完成:成功 $created,跳过 $skipped,失败 $failed" log "初始密码见 $RESULT,请尽快分发并要求用户首次登录后修改"幂等这块是重点。批量导入脚本最大的风险是被误跑第二次。user_exists用occ user:info的退出码来判断,比解析user:list的输出快得多——后者要拉全量用户列表,几百人的实例上每次调用都很慢。这里判断存在就直接跳过,不会覆盖已有账号的密码(occ user:add遇到已存在用户本来就报错,但提前判断能少打一次昂贵的 bootstrap)。
参数拼接用了 bash 数组args+=(...),而不是${dname:+--display-name="$dname"}这种花活。原因很直接:参数展开的结果不会重新进行引号解析,显示名里带空格时会被拆成多个单词,occ拿到一堆垃圾参数,报错信息还特别难懂。数组是唯一正确的做法。
输入重定向那一行同时干掉了两个经典的坑:sed '1s/^\xEF\xBB\xBF//'去掉 Windows 记事本另存为 UTF-8 时加上的 BOM,tr -d '\r'去掉 CRLF 行尾。这两个东西如果留着,第一个用户名前面会莫名多出三个不可见字节,或者最后一个字段尾巴上挂着\r,表现出来就是「账号明明建好了,密码输进去却提示错误」。这个坑下面还会详细讲。
用< <(...)进程替换而不用管道,是为了让循环体在当前 shell 里执行。如果用cat file | while read ...,while会跑在一个子 shell 里,循环结束后created、failed这些计数器全部归零,统计输出永远是 0。这是 bash 新手最常撞的墙之一。
3.4 什么时候可以并发,什么时候绝对不行
单线程跑 100 个用户,按每个 1.5 秒算,大概两分半。跑 1000 个就是 25 分钟,可以接受。但如果你的实例有几千个用户要建,就会开始想并行。
并行的可行套路是把输入文件切片,起多个进程分别处理:
split -n l/4 users.psv chunk_ for f in chunk_*; do ./nc_bulk_user_add.sh "$f" & done wait但我必须提醒:并发数不要超过 4,而且在并发场景下要特别小心三件事。第一,数据库写入竞争。多个occ进程同时创建用户,会在oc_users、oc_accounts、oc_storages等表上产生锁等待,MySQL 默认的行锁加上 Nextcloud 的事务粒度,很可能出现死锁,重试逻辑没写好就会丢账号。第二,组创建竞争。这就是我在循环里先显式调group:add的原因,虽然它会报「组已存在」的错误,但至少是可控的错误,不会像两个进程同时创建同一个组那样出现奇怪的中间状态。第三,日志交叉。多个进程写同一个日志文件会互相插行,所以我给每个进程生成的日志名带了时间戳,并行时最好再加个$$进程号。
我的实际建议是:一千人以下就单线程慢慢跑,泡杯茶的事。真要上几千人,别硬堆并发,改用 OCS Provisioning API,它是为这个场景设计的,一次 HTTP 请求一个用户,用xargs -P或者简单的 Python 脚本并发几十路都不会有问题。创建用户的接口大致是:
curl -s -X POST \ -H "OCS-APIRequest: true" \ -u "admin:应用密码" \ "https://cloud.example.com/ocs/v2.php/cloud/users" \ -d "userid=zhangwei" \ -d "password=临时密码" \ -d "displayName=张伟" \ -d "email=zhangwei@example.com" \ -d "groups[]=staff"注意这里的密码不能用刚才那些自动生成的弱密码——接口会走完整的密码策略校验,太简单的会被拒绝。应用密码可以在管理后台的「安全」页面里生成,某些版本也支持用occ user:add-app-password <uid>直接创建。
4. 批量删除与事后对账:误删一次就够记一辈子
创建脚本写错了,最坏结果是多几个账号要清理。删除脚本写错了,就是数据永久丢失。这两个操作的心理负担完全不对等,所以删除脚本必须多一层保护。
4.1 先跑 dry-run,再跑禁用,最后才真删
我的删除脚本强制要求显式指定模式,不给默认行为:
#!/usr/bin/env bash # # nc_bulk_user_del.sh - 批量删除 Nextcloud 用户 # 用法: sudo -u www-data ./nc_bulk_user_del.sh list.psv [--dry-run|--disable|--delete] # set -uo pipefail OCC="/var/www/nextcloud/occ" PHPBIN="php" LIST="${1:-}" MODE="${2:---dry-run}" TS="$(date +%Y%m%d_%H%M%S)" LOG="./nc_del_${TS}.log" [[ -z "$LIST" || ! -f "$LIST" ]] && { echo "用法: $0 <list.psv> [--dry-run|--disable|--delete]" >&2; exit 1; } occ_run() { "$PHPBIN" "$OCC" -n "$@" 2>>"$LOG"; } log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG"; } while IFS= read -r line; do uid="$(printf '%s' "$line" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" [[ -z "$uid" || "$uid" == \#* ]] && continue if ! occ_run user:info "$uid" >/dev/null 2>&1; then log "SKIP $uid 不存在" continue fi last="$(occ_run user:lastseen "$uid" 2>/dev/null | tail -1)" log "INFO $uid 最后活动时间:$last" case "$MODE" in --dry-run) log "DRY $uid 将执行禁用+删除(未实际执行)" ;; --disable) occ_run user:disable "$uid" && log "DIS $uid 已禁用" ;; --delete) occ_run user:delete "$uid" && log "DEL $uid 已删除" || log "FAIL $uid 删除失败" ;; *) log "ERR 未知模式 $MODE" exit 2 ;; esac done < "$LIST" log "处理完成,模式=$MODE"dry-run 模式的价值在于它会把每个账号的最后活动时间打印出来。这个信息非常有用:一个三个月没登录的账号和一个昨天还在用的账号,处理优先级完全不同。我会把 dry-run 的输出导出来给业务方确认,签字之后再跑--disable。
--delete模式我刻意没有做「先禁用再删除」的自动串联。因为禁用和删除之间必须留观察期,这个时间跨度由人来判断,不能写死在脚本里。
4.2 删除之后怎么对账
删完之后别急着关终端,做三件事核对。
第一,查用户总数变化。occ user:report会输出一张小表,包含用户数、分组数、各存储的占用情况:
sudo -u www-data php /var/www/nextcloud/occ user:report跑删除前跑一次,跑完再跑一次,两个数字对得上,说明批量操作没有漏删或者多删。
第二,看数据目录是否清干净:
sudo -u www-data find /var/www/nextcloud/data -maxdepth 1 -type d -printf '%f\n' | sort正常情况下,被删用户对应的目录应该消失了。如果残留,说明删除过程中途失败,这类目录可以手工确认后再删,但一定要确认目录名对应的账号确实注销了。
第三,检查数据库里有没有孤儿记录。绝大多数情况下occ user:delete会把关联记录清理干净,但如果之前用 SQL 直接删过用户,或者从旧版本升级上来,oc_preferences、oc_accounts里可能有残留。查残留的方式是拿数据目录和账号表做差集:
sudo -u www-data php /var/www/nextcloud/occ user:list --output=json \ | python3 -c 'import json,sys; [print(k) for k in json.load(sys.stdin)]' \ | sort > /tmp/nc_users.txt comm -13 /tmp/nc_users.txt <(ls /var/www/nextcloud/data | sort)左边是你的账号列表,右边是数据目录列表,comm -13输出的是「有目录但没账号」的那部分。这个结果不一定是错误——appdata_xxx、files_external、__groupfolders这些都是系统目录,属于正常范畴。把系统目录排除掉再看,剩下的才需要关注。
5. 我在真实实例上踩到的六个坑与排查链路
这一节值得单独拎出来,因为下面每一条都是我在实际环境里撞过的,而且每一条的症状和根因之间的关联都相当反直觉。
5.1 root 跑 occ 之后,网页端突然 500
症状是网页访问报 500,但occ status明明显示正常。排查链路:先看 Nextcloud 日志尾部tail -50 data/nextcloud.log,会看到权限相关的报错,类似无法写入 appdata 或者无法创建文件。再确认文件属主:
ls -l /var/www/nextcloud/data/ | head看到root root的那一刻就明白了。修复方式是整树改回 web 用户:
chown -R www-data:www-data /var/www/nextcloud find /var/www/nextcloud -type d -exec chmod 750 {} \; find /var/www/nextcloud -type f -exec chmod 640 {} \;注意别用chmod -R 777这种粗暴做法,Nextcloud 对权限有要求,777 会触发安全检查警告,某些版本还会直接拒绝启动。
5.2 用户名明明正确,密码却提示错误
这个坑的隐蔽性很高。账号在后台看得到,用户名也是对的,但用户拿着我们发的初始密码就是登不进去。排查方式:把users.psv用十六进制看一眼。
head -2 users.psv | cat -A如果行尾出现^M$,说明文件是 CRLF 换行,Windows 上编辑过的文件都会这样。此时read拿到的最后一个字段尾部会带一个不可见的回车符。如果那一列正好是密码,密码就变成xxx\r,用户输入xxx当然对不上。更糟的情况是用户名列在最后,那用户名里就带上了\r,登录时输入的「正确用户名」和系统里存的「正确用户名\r」不是同一个东西。
修复很简单,脚本里那一行tr -d '\r'就是干这个的。但如果文件里已经建了一批带\r的账号,清理起来要用occ user:list --output=json把用户名 dump 出来,逐个确认哪些是脏的,然后用occ user:delete清掉重建。所以养成习惯:任何从 Windows 传过来的文本文件,先跑一遍dos2unix或者sed -i 's/\r$//'再喂给脚本。
同一个坑的另一个变体是 BOM。用记事本「另存为 UTF-8」时默认会加 BOM,表现为第一个用户名前面多出三个字节。日志里看起来是username,复制粘贴出来又找不到任何异常字符,因为它是不可见字符。脚本里那个sed '1s/^\xEF\xBB\xBF//'就是专门对付它的。
5.3 密码策略把批量创建拦了一大半
现象是脚本跑到一半开始大面积报错,日志里出现类似密码长度不足或者复杂度不够的提示。根因是 Nextcloud 默认启用了密码策略应用,它对所有创建密码的入口都生效,包括命令行。
排查方式是确认这个应用是否启用:
sudo -u www-data php /var/www/nextcloud/occ app:list | grep -i password处理方式有三种,我按推荐程度排:第一,改脚本的密码生成规则,让它符合策略要求,这也是我在gen_pass里拼Aa1的原因;第二,临时禁用密码策略应用,批量导完再启用,但要注意禁用期间所有账号都可能设弱密码;第三,在策略配置里为特定用户组放宽要求,适合那种「这批是内网测试账号」的场景。
我一般选第一种。为了让脚本生成的密码可控,可以在脚本里加一段「生成后本地校验」的逻辑,长度不够就重新生成,这样失败率能压到零。
5.4 大批量导入后的性能观察
建完 500 个账号之后,我注意到几个现象。首先,occ user:list明显变慢了,从不到一秒变成四五秒,因为它要遍历所有用户。所以脚本里用user:info做存在性判断而不是user:list,这个选择在用户量上去之后收益很大。
其次是数据库层面。oc_accounts和oc_preferences表随着用户数增长,如果没有合适的索引,后台的用户管理页面会越来越卡。Nextcloud 提供了索引检查命令:
sudo -u www-data php /var/www/nextcloud/occ db:add-missing-indices这个命令是幂等的,跑多少次都安全,建议每次大批量操作后都执行一遍。同类的还有occ db:add-missing-columns、occ db:add-missing-primary-keys,都是升级和批量操作后值得跑的健康检查。
5.5 组存在的判断不能靠退出码
occ group:add在组已存在时返回非零退出码。这意味着你不能用「命令成功就说明组新建了」这种逻辑,因为「组已存在」和「创建失败」在这条命令上返回的是同一类结果。更稳妥的做法是先用occ group:list拿列表再判断,但那个命令在组多的时候也不快。我的处理方式是接受这个模糊性——反正后续user:add --group会自动处理组的存在性,显式建组只是为了在并发场景下减少竞争,报个警告继续跑就行。
5.6 删除用户时被外部共享卡住
有一次删除某个账号,occ user:delete跑了很久然后报错退出。查日志发现是这个用户有未完成的外部存储挂载配置,删除流程在清理阶段卡住了。处理方式是先手工移除该用户的外部存储挂载,再执行删除:
sudo -u www-data php /var/www/nextcloud/occ files_external:list <uid> sudo -u www-data php /var/www/nextcloud/occ files_external:delete --yes <mount_id>这个坑的教训是:删除之前先看一眼这个账号有没有配置外部存储、有没有加入特殊的组、有没有被设置为某个组的管理员。这些关联关系都会在删除流程里被处理,但处理顺序和失败处理做得不一定完善,提前清掉能让删除顺利得多。
6. 上线前的验证流程:先在本地小实例把脚本跑通
不管脚本看起来多完美,直接在生产环境跑都是不专业的。我的做法是在本地先用一个小实例走一遍完整流程。
6.1 用本地实例做验证环境
如果你手头有闲置机器,用 Ubuntu 装一个 Nextcloud 是最省事的,主流发行版的软件源里就有打包好的版本,装完配好数据库就能跑。Windows 用户可以用 WSL2 里的 Ubuntu,装一个实例专门用来测脚本,好处是隔离彻底,跑崩了直接重置。也有人用虚拟机做这件事,建一个快照,跑完测试回滚,同样的输入能得到同样的初始状态,对复现问题特别有用。
本地验证环境不需要和生产环境配置完全一致,但有几个关键点要对齐:PHP 大版本尽量一致,Nextcloud 大版本必须一致(命令参数在不同大版本之间有变化),密码策略应用的启用状态要一致,否则你测出来的结果在生产上不成立。
验证时我建议准备三组测试数据:一组正常数据,覆盖所有字段;一组边界数据,包含空字段、超长显示名、特殊字符密码;一组脏数据,故意混入 CRLF、BOM、空行、重复用户名、格式错误的行。脚本能正确处理这三组数据,才算基本可用。
6.2 上线前的检查清单
正式执行前,我会核对下面这些项:
| 检查项 | 确认方式 | 为什么重要 |
|---|---|---|
| 数据库已在跑 | occ status输出正常 | 避免误判为命令出错 |
| 已开启维护模式 | occ maintenance:mode --on | 大批量写入期间避免前台请求干扰 |
| 输入文件编码正确 | file users.psv看是否为 UTF-8 | BOM 和 GBK 都会导致用户名错乱 |
| 输入文件无 CRLF | grep -c $'\r' users.psv应为 0 | 行尾控制符是最高频的坑 |
| 用户名无重复 | cut -d'|' -f1 users.psv | sort | uniq -d | 重复会导致后一条报错 |
| 密码策略已适配 | 先在测试实例建一个账号验证 | 避免跑到一半大面积失败 |
| 有数据库备份 | 确认最近的备份时间点 | 删除操作的最后一道保险 |
维护模式那一项值得特别说明。occ maintenance:mode --on会让网页端进入维护页面,用户访问时看到的是提示而不是报错,这比让几百个用户在我们批量写数据库的时候疯狂刷新页面要友好得多。等脚本跑完,再用occ maintenance:mode --off关掉,中间如果有用户刚好在操作,损失最小。
另外,批量操作完成后建议跑一次occ files:scan --all刷新文件缓存,虽然新建账号没有文件,但这个命令能顺带修复一些因为并发写入导致的缓存不一致。它在大实例上可能跑很久,所以安排在业务低峰期执行。
最后一句实话:这类脚本我从不追求一次性写完美。第一版只要能跑通、能记录日志就够了,然后根据实际情况增删功能。我第一版脚本连幂等判断都没有,第二次误跑把几百个账号的密码全重置了一遍——那之后我才加上了user_exists检查。踩过坑加进去的代码,比一开始就设计出来的功能,往往更贴合真实需求。