Another status update. This is my current progress which will install the files, and seemingly builds the system out, but still is very buggy. I am going to keep working on it until we get it right, but I don't like not providing some show of progress. The reason its so slow is this project although a Django project, has a lot of internal standards that don't conform to the best practices or norms. This is fine, but it makes learning it and adapting it much slower than I had anticipated. It does seem like a mature project so worthy of the time. Thanks for your patience. If you wanted to experiment place this python file someplace available online and use the following curl command to invoke it,
curl -s -X POST https://my.opalstack.com/api/v1/app/create/ \ ✔
-H "Authorization: Token 1234" \
-H "Content-Type: application/json" \
-d '[{
"name": "seafile",
"osuser": "UUID",
"type": "CUS",
"installer_url": "https://foo.com/seafile.py",
"json": {}
}]'
Their official docs are here,
https://manual.seafile.com/12.0/setup_binary/installation_ce/#create-the-env-file-in-conf-directory

#!/usr/bin/env python3
# Seafile installer for Opalstack (EL9) - CE 12.0 aligned (no Pro/seafevents)
# - Provisions MariaDB user + 3 DBs (ccnet, seafile, seahub), per official docs.
# - Runs setup-seafile-mysql.sh (auto, use existing DBs) with streamed output.
# - Creates a venv and installs seahub/requirements.txt.
# - Writes conf/.env with JWT_PRIVATE_KEY and hostname (12.x requires JWT key).
# - Removes conf/seafevents.conf so CE won't try to load Pro seafevents.
# - Runs Django migrations (verbose) and auto-creates admin (no prompt)
# using the account email fetched from /api/v1/account/info/.
# - Binds Gunicorn to the main app port from /app/read.
# - Auto-starts services and curls localhost:<main_port>.
import argparse
import http.client
import json
import logging
import os
import os.path
import secrets
import shlex
import string
import subprocess
import sys
import time
from urllib.parse import urlparse
from string import Template
API_BASE_URI = "/api/v1"
SEAFILE_DEFAULT_VERSION = "12.0.14"
SEAFILE_URL_FMT_PRIMARY = "https://s3.eu-central-1.amazonaws.com/download.seadrive.org/seafile-server_{ver}_x86-64.tar.gz"
SEAFILE_URL_FMT_FALLBACK = "https://s3.eu-central-1.amazonaws.com/download.seafile.com/seafile-server_{ver}_x86-64.tar.gz"
CMD_ENV = {"PATH": "/usr/local/bin:/usr/bin:/bin", "UMASK": "0002"}
# ---------- helpers ----------
def gen_password(length=20):
chars = string.ascii_letters + string.digits
return "".join(secrets.choice(chars) for _ in range(length))
def gen_jwt_key(length=40):
chars = string.ascii_letters + string.digits
return "".join(secrets.choice(chars) for _ in range(length))
def run_command(cmd, env=None, cwd=None, shellish=False, allow_fail=False):
if env is None:
env = CMD_ENV
logging.info(("RUN (allow-fail): " if allow_fail else "RUN: ") + cmd)
try:
if shellish:
out = subprocess.check_output(cmd, env=env, cwd=cwd, stderr=subprocess.STDOUT, shell=True)
else:
out = subprocess.check_output(shlex.split(cmd), env=env, cwd=cwd, stderr=subprocess.STDOUT)
if out:
logging.debug(out.decode("utf-8", errors="ignore"))
return out
except subprocess.CalledProcessError as e:
if allow_fail:
try:
logging.info(e.output.decode("utf-8", errors="ignore"))
except Exception:
pass
return None
logging.error(f"Command failed rc={e.returncode}")
try:
logging.error(e.output.decode("utf-8", errors="ignore"))
except Exception:
pass
sys.exit(1)
def stream_command(cmd, env=None, cwd=None):
"""Live-stream stdout/stderr of a long-running command to the installer log and fail fast on nonzero rc."""
if env is None:
env = CMD_ENV
logging.info("RUN (stream): " + (cmd if isinstance(cmd, str) else " ".join(shlex.quote(x) for x in cmd)))
proc = subprocess.Popen(
cmd if isinstance(cmd, list) else shlex.split(cmd),
env=env,
cwd=cwd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
assert proc.stdout is not None
for line in proc.stdout:
logging.info(line.rstrip("\n"))
rc = proc.wait()
if rc != 0:
logging.error(f"Command failed rc={rc}")
sys.exit(rc)
logging.info("Done.\n")
def create_file(path, contents, perms=0o600, mode="w"):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, mode) as f:
f.write(contents)
os.chmod(path, perms)
logging.info(f"Created file {path} (mode {oct(perms)})")
def _require_env(name):
v = os.environ.get(name)
if not v:
logging.error(f"Required env var {name} not set")
sys.exit(1)
return v
def _parse_api_host_from_env():
api_url = _require_env("API_URL")
p = urlparse(api_url)
host = p.netloc or p.path
host = host.strip("/").strip()
if not host:
logging.error(f"Could not parse API_URL={api_url!r} into host")
sys.exit(1)
return host
# ---------- Opalstack API ----------
class OpalstackAPITool:
def __init__(self, host, authtoken, user, password):
self.host = host
self.base_uri = API_BASE_URI
if not authtoken:
payload = json.dumps({"username": user, "password": password})
conn = http.client.HTTPSConnection(self.host)
conn.request("POST", self.base_uri + "/login/", payload, headers={"Content-type": "application/json"})
resp = conn.getresponse()
body = resp.read()
try:
result = json.loads(body)
except Exception:
logging.error(f"Login failed, non-JSON: {body[:200]!r}")
sys.exit(1)
token = result.get("token")
if not token:
logging.error("Invalid username/password and no OPAL_TOKEN provided")
sys.exit(1)
authtoken = token
self.headers = {"Content-type": "application/json", "Authorization": f"Token {authtoken}"}
def get(self, endpoint):
uri = self.base_uri + endpoint
conn = http.client.HTTPSConnection(self.host)
conn.request("GET", uri, headers=self.headers)
resp = conn.getresponse()
body = resp.read()
try:
data = json.loads(body)
except Exception:
logging.error(f"GET {uri} non-JSON: {body[:200]!r}")
sys.exit(1)
if resp.status >= 300:
logging.error(f"GET {uri} http={resp.status} body={data}")
sys.exit(1)
logging.info(f"GET {uri} -> ok")
return data
def post(self, endpoint, payload_obj):
uri = self.base_uri + endpoint
data = json.dumps(payload_obj)
conn = http.client.HTTPSConnection(self.host)
conn.request("POST", uri, data, headers=self.headers)
resp = conn.getresponse()
body = resp.read()
text = body.decode("utf-8", errors="ignore")
try:
parsed = json.loads(text)
except Exception:
parsed = text
if resp.status >= 300:
logging.error(f"POST {uri} http={resp.status} body={parsed}")
sys.exit(1)
logging.info(f"POST {uri} -> ok")
return parsed
def post_allow_4xx(self, endpoint, payload_obj):
uri = self.base_uri + endpoint
data = json.dumps(payload_obj)
conn = http.client.HTTPSConnection(self.host)
conn.request("POST", uri, data, headers=self.headers)
resp = conn.getresponse()
body = resp.read()
text = body.decode("utf-8", errors="ignore")
try:
parsed = json.loads(text)
except Exception:
parsed = text
return resp.status, parsed
# ---------- waits ----------
def wait_for_mariauser_ready(api, user_id, password_to_set, timeout_sec=180):
deadline = time.time() + timeout_sec
sleep_s = 2
while True:
status, body = api.post_allow_4xx("/mariauser/update/", [{"id": user_id, "json": {"password": password_to_set}}])
if status < 300:
logging.info("MariaDB user is ready (password set).")
return
body_str = body if isinstance(body, str) else json.dumps(body)
if status == 400 and "Not ready" in body_str:
logging.info(f"MariaDB user not ready yet, retrying in {sleep_s}s...")
time.sleep(sleep_s)
sleep_s = min(sleep_s + 1, 10)
if time.time() > deadline:
logging.error("Timeout waiting for MariaDB user to become ready.")
sys.exit(1)
continue
logging.error(f"/mariauser/update/ failed: http={status} body={body}")
sys.exit(1)
# ---------- seafile assets ----------
def pick_seafile_url(version, override_url):
if override_url:
return override_url
primary = SEAFILE_URL_FMT_PRIMARY.format(ver=version)
fallback = SEAFILE_URL_FMT_FALLBACK.format(ver=version)
rc = subprocess.call(shlex.split(f"/usr/bin/wget -q --spider {shlex.quote(primary)}"), env=CMD_ENV)
if rc == 0:
logging.info(f"Seafile tarball URL (primary) OK: {primary}")
return primary
logging.info(f"Primary URL failed (--spider rc={rc}), trying fallback: {fallback}")
rc2 = subprocess.call(shlex.split(f"/usr/bin/wget -q --spider {shlex.quote(fallback)}"), env=CMD_ENV)
if rc2 == 0:
logging.info(f"Seafile tarball URL (fallback) OK: {fallback}")
return fallback
logging.error("Could not verify Seafile download URL (both primary and fallback failed).")
logging.error(f"Primary tried: {primary}")
logging.error(f"Fallback tried: {fallback}")
sys.exit(1)
def read_seafile_data_dir(ccnet_dir):
ini_path = os.path.join(ccnet_dir, "seafile.ini")
if not os.path.exists(ini_path):
return None
try:
with open(ini_path, "r") as f:
line = f.readline().strip()
return line or None
except Exception:
return None
def write_seafile_data_dir(ccnet_dir, path):
os.makedirs(ccnet_dir, exist_ok=True)
create_file(os.path.join(ccnet_dir, "seafile.ini"), path.rstrip() + "\n", 0o600)
# ---------- venv + requirements ----------
def ensure_virtualenv(appdir):
venv_dir = os.path.join(appdir, "venv")
py = os.path.join(venv_dir, "bin", "python")
pip = os.path.join(venv_dir, "bin", "pip")
if not os.path.exists(py):
run_command(f"/usr/bin/python3 -m venv {shlex.quote(venv_dir)}")
run_command(f"{shlex.quote(pip)} install --no-cache-dir --upgrade pip setuptools")
return venv_dir, py, pip
# (The install_requirements_filtered function is not needed because we'll install specific packages below)
# ---------- admin email ----------
def pick_admin_email(api, appinfo, server_domain):
# 1) explicit override env (if you set it)
env_email = os.environ.get("OPAL_ACCOUNT_EMAIL")
if env_email:
return env_email.strip()
# 2) authoritative Opalstack current account endpoint
try:
info = api.get("/account/info/")
if isinstance(info, dict):
for key in ("email", "email_address", "username"):
v = info.get(key)
if v and "@" in v:
return v
except SystemExit:
raise
except Exception:
pass
# 3) fallback: osuser@domain
osuser = appinfo.get("osuser_name") or "user"
return f"{osuser}@{server_domain}"
# ---------- main ----------
def main():
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s: %(message)s", force=True)
logging.info("Seafile installer starting up (CE 12.0, venv + detailed requirements installation)")
p = argparse.ArgumentParser(description="Install Seafile on Opalstack")
p.add_argument("-i", "--app-uuid", dest="app_uuid", default=os.environ.get("UUID"))
p.add_argument("-n", "--app-name", dest="app_name", default=os.environ.get("APPNAME"))
p.add_argument("-t", "--opal-token",dest="opal_token",default=os.environ.get("OPAL_TOKEN"))
p.add_argument("-u", "--opal-user", dest="opal_user", default=os.environ.get("OPAL_USER"))
p.add_argument("-p", "--opal-pass", dest="opal_pass", default=os.environ.get("OPAL_PASS"))
p.add_argument("--db-user", dest="db_user_name", default=None)
p.add_argument("--db-prefix", dest="db_prefix", default=None)
p.add_argument("--cus-app-name", dest="cus_app_name", default=None)
p.add_argument("--mysql-host", dest="mysql_host", default=os.environ.get("MYSQL_HOST", "127.0.0.1"))
p.add_argument("--mysql-port", dest="mysql_port", default=os.environ.get("MYSQL_PORT", "3306"))
p.add_argument("--seafile-version", dest="seafile_version", default=SEAFILE_DEFAULT_VERSION)
p.add_argument("--seafile-url", dest="seafile_url", default=None)
p.add_argument("--domain", dest="domain", default=None, help="Domain for setup (default: <app>.fake.local)")
args = p.parse_args()
if not args.app_uuid or not args.app_name:
logging.error("Missing -i <UUID> and/or -n <APPNAME>")
sys.exit(1)
api_host = _parse_api_host_from_env()
if not args.opal_token and (not args.opal_user or not args.opal_pass):
logging.error("Provide OPAL_TOKEN (-t) OR OPAL_USER/-u and OPAL_PASS/-p")
sys.exit(1)
logging.info(f"API host: {api_host}")
logging.info(f"App: {args.app_name} ({args.app_uuid})")
api = OpalstackAPITool(api_host, args.opal_token or "", args.opal_user or "", args.opal_pass or "")
# App info (need osuser, server, assigned port)
appinfo = api.get(f"/app/read/{args.app_uuid}")
osuser_name = appinfo.get("osuser_name")
main_app_port = appinfo.get("port")
if not osuser_name:
logging.error("app/read missing osuser_name")
sys.exit(1)
if not main_app_port:
logging.error("app/read missing port for main app; cannot bind Seahub without it")
sys.exit(1)
appdir = f"/home/{osuser_name}/apps/{appinfo['name']}"
for d in ("tmp", ".cache", "bin", "var", "log", "logs", "pids"):
os.makedirs(os.path.join(appdir, d), exist_ok=True)
CMD_ENV["HOME"] = f"/home/{osuser_name}"
CMD_ENV["USER"] = osuser_name
logging.info(f"Prepared appdir: {appdir}")
# Domain for setup
domains = appinfo.get("domains") or []
server_domain = args.domain or (domains[0] if domains else f"{args.app_name}.fake.local")
server_domain = str(server_domain).strip()
logging.info(f"Using domain for setup: {server_domain}")
# CUS app (seafhttp)
cus_name = args.cus_app_name or f"{appinfo['name']}_fs"
cus_app_uuid = None
cus_app_port = None
all_apps = api.get("/app/list/")
for a in all_apps:
if a.get("name") == cus_name and a.get("type") == "CUS" and a.get("osuser") == appinfo.get("osuser"):
cus_app_uuid = a.get("id")
cus_app_port = a.get("port")
break
if not cus_app_uuid:
created = api.post("/app/create/", [{
"osuser": appinfo["osuser"],
"name": cus_name,
"type": "CUS",
"json": {"proxy_pass_trailing_slash": True}
}])
if not isinstance(created, list) or not created:
logging.error(f"Unexpected /app/create/ response: {created}")
sys.exit(1)
cus_app_uuid = created[0].get("id")
# Wait for CUS app to receive a port
deadline = time.time() + 180
while time.time() < deadline:
info = api.get(f"/app/read/{cus_app_uuid}")
cus_app_port = info.get("port")
if cus_app_port:
break
time.sleep(2)
if not cus_app_port:
logging.error("CUS app did not receive a port")
sys.exit(1)
logging.info(f"CUS app ready: {cus_name} port={cus_app_port}")
# MariaDB user + databases
short_app = (args.app_name or "app")[:8]
short_uuid = (args.app_uuid or "uuid")[:8]
db_prefix = args.db_prefix or f"{short_app}_{short_uuid}"
db_user_name = args.db_user_name or db_prefix
ures = api.post("/mariauser/create/", [{"name": db_user_name, "server": appinfo["server"]}])
if not isinstance(ures, list) or not ures:
logging.error(f"Unexpected /mariauser/create/ response: {ures}")
sys.exit(1)
db_user_id = ures[0]["id"]
db_user_real = ures[0]["name"]
db_user_pass = ures[0].get("default_password") or gen_password(20)
wait_for_mariauser_ready(api, db_user_id, db_user_pass, timeout_sec=180)
def mkdb(name):
dres = api.post("/mariadb/create/", [{
"name": name,
"server": appinfo["server"],
"charset": "utf8mb4",
"dbusers_readwrite": [db_user_id]
}])
if not isinstance(dres, list) or not dres:
logging.error(f"Unexpected /mariadb/create/ response: {dres}")
sys.exit(1)
return dres[0]["id"]
ccnet_db = f"{db_prefix}_ccnet"
seafile_db = f"{db_prefix}_seafile"
seahub_db = f"{db_prefix}_seahub"
mkdb(ccnet_db); mkdb(seafile_db); mkdb(seahub_db)
logging.info(f"Databases created: {ccnet_db}, {seafile_db}, {seahub_db}")
# Download and extract Seafile server package
seafile_url = pick_seafile_url(args.seafile_version, args.seafile_url)
tar_path = os.path.join(appdir, ".cache", "seafile.tar.gz")
run_command(f"/usr/bin/wget -O {shlex.quote(tar_path)} {shlex.quote(seafile_url)}")
run_command(f"tar -xzvf {shlex.quote(tar_path)} -C {shlex.quote(appdir)}")
# Locate the extracted Seafile server directory
seafile_root = None
for name in os.listdir(appdir):
p = os.path.join(appdir, name)
if name.startswith("seafile-server-") and os.path.isdir(p):
seafile_root = p
break
if not seafile_root:
logging.error("Could not locate extracted seafile-server-* directory")
sys.exit(1)
# Prepare data directory (avoid collision if 'data' exists)
base_data_dir = os.path.join(appdir, "data")
data_dir = base_data_dir if not os.path.exists(base_data_dir) else os.path.join(appdir, f"data-{int(time.time())}")
if data_dir != base_data_dir:
logging.info(f"{base_data_dir} already exists; using {data_dir} for setup")
# Run Seafile setup (non-interactive) with existing DBs (-e 1) and stream output
setup_script = os.path.join(seafile_root, "setup-seafile-mysql.sh")
if not os.path.exists(setup_script):
logging.error(f"Missing {setup_script}")
sys.exit(1)
setup_cmd = (
f"{setup_script} auto "
f"-n {shlex.quote(args.app_name)} "
f"-i {shlex.quote(server_domain)} "
f"-p {shlex.quote(str(cus_app_port))} "
f"-d {shlex.quote(data_dir)} "
f"-e 1 "
f"-o {shlex.quote(args.mysql_host)} "
f"-t {shlex.quote(str(args.mysql_port))} "
f"-u {shlex.quote(db_user_real)} "
f"-w {shlex.quote(db_user_pass)} "
f"-c {shlex.quote(ccnet_db)} "
f"-s {shlex.quote(seafile_db)} "
f"-b {shlex.quote(seahub_db)} "
)
stream_command(f"/bin/bash -lc {shlex.quote(setup_cmd)}", cwd=seafile_root)
# Create required config files in conf/
conf_dir = os.path.join(appdir, "conf")
os.makedirs(conf_dir, exist_ok=True)
jwt_key = gen_jwt_key(40)
env_text = (
"TIME_ZONE=UTC\n"
f"JWT_PRIVATE_KEY={jwt_key}\n"
"SEAFILE_SERVER_PROTOCOL=http\n"
f"SEAFILE_SERVER_HOSTNAME={server_domain}\n"
f"SEAFILE_MYSQL_DB_HOST={args.mysql_host}\n"
f"SEAFILE_MYSQL_DB_PORT={args.mysql_port}\n"
f"SEAFILE_MYSQL_DB_USER={db_user_real}\n"
f"SEAFILE_MYSQL_DB_PASSWORD={db_user_pass}\n"
f"SEAFILE_MYSQL_DB_CCNET_DB_NAME={ccnet_db}\n"
f"SEAFILE_MYSQL_DB_SEAFILE_DB_NAME={seafile_db}\n"
f"SEAFILE_MYSQL_DB_SEAHUB_DB_NAME={seahub_db}\n"
)
create_file(os.path.join(conf_dir, ".env"), env_text, 0o600)
# Remove seafevents.conf if present (CE must not load Pro seafevents)
seafevents_conf = os.path.join(conf_dir, "seafevents.conf")
if os.path.exists(seafevents_conf):
try:
os.remove(seafevents_conf)
logging.info("Removed conf/seafevents.conf (CE must not load Pro seafevents).")
except Exception as e:
logging.warning(f"Could not remove conf/seafevents.conf: {e}")
# Ensure ccnet/seafile.ini exists and points to seafile-data (some tools expect it)
ccnet_dir = os.path.join(appdir, "ccnet")
want_data = read_seafile_data_dir(ccnet_dir)
if not want_data:
want_data = os.path.join(appdir, "seafile-data")
logging.warning(f"Missing ccnet/seafile.ini; creating it to point at {want_data}")
write_seafile_data_dir(ccnet_dir, want_data)
if not os.path.isabs(want_data):
want_data = os.path.abspath(os.path.join(appdir, want_data))
os.makedirs(want_data, exist_ok=True)
# --- Python virtualenv and requirements ---
venv_dir, vpy, vpip = ensure_virtualenv(appdir)
# Install required Python packages (from official CE 12.0 manual).
# You can comment out individual packages below for debugging if needed.
# Core, needed to boot Seahub
pip_packages = [
"django==4.2.*",
"future==1.0.*",
"mysqlclient==2.2.*", # keep: we use MySQL/MariaDB
"pymysql", # keep as fallback client
"pillow==10.4.*",
"captcha==0.6.*",
"markupsafe==2.0.1",
"jinja2",
"sqlalchemy==2.0.*",
"pycryptodome==3.20.*",
"cffi==1.17.0",
"lxml",
# Optional / commonly-problematic on shared hosts:
# "python-ldap==3.4.*", # LDAP/AD auth; needs OpenLDAP & SASL headers. Commented out to avoid build failure.
# "pylibmc", # memcached client; needs libmemcached at runtime. Optional.
# "django-pylibmc", # Django binding; optional.
# "djangosaml2==1.9.*", # SAML SSO; optional.
# "pysaml2==7.3.*", # SAML dependency; optional.
# "psd-tools", # PSD previews; drags in numpy/scipy/scikit-image. Optional.
# "gevent==24.2.*", # Only needed if using gevent workers; we use default sync workers.
]
# Stream pip installation for verbose output
stream_command([vpip, "install", "--no-cache-dir", "--verbose"] + pip_packages)
# Gunicorn config: bind to the main app port
gunicorn_conf = os.path.join(conf_dir, "gunicorn.conf.py")
gunicorn_cfg = (
"bind = '0.0.0.0:{port}'\n"
"workers = 2\n"
"timeout = 120\n"
"accesslog = '-'\n"
"errorlog = '-'\n"
).format(port=int(main_app_port))
create_file(gunicorn_conf, gunicorn_cfg, 0o600)
# Wrapper scripts to start/stop Seafile and Seahub
start_sh = f"""#!/bin/bash
set -e
export LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
. {shlex.quote(os.path.join(venv_dir, "bin", "activate"))}
export SEAFILE_CENTRAL_CONF_DIR={shlex.quote(conf_dir)}
export GUNICORN_CONF="$SEAFILE_CENTRAL_CONF_DIR/gunicorn.conf.py"
cd {shlex.quote(seafile_root)}
# Load .env to environment (optional for CLI tools)
if [ -f "$SEAFILE_CENTRAL_CONF_DIR/.env" ]; then
set -a; . "$SEAFILE_CENTRAL_CONF_DIR/.env"; set +a
fi
export SEAHUB_PORT={int(main_app_port)}
./seafile.sh start
./seahub.sh start "$SEAHUB_PORT"
"""
stop_sh = f"""#!/bin/bash
cd {shlex.quote(seafile_root)}
./seahub.sh stop || true
./seafile.sh stop || true
"""
create_file(os.path.join(appdir, "start.sh"), start_sh, 0o700)
create_file(os.path.join(appdir, "stop.sh"), stop_sh, 0o700)
# README
readme_tpl = Template("""Seafile Community Edition v$seafile_version installed (venv; base requirements)
Ports:
Seahub (main app/gunicorn): $main_app_port
seafhttp (CUS app): $cus_app_port
Python:
Virtualenv: $venv_dir
Paths:
install root: $seafile_root
conf: $conf_dir
ccnet: $ccnet_dir
seafile-data: $data_dir
Domain/URL:
Base URL: http://$server_domain
Database (MariaDB):
host: $mysql_host
port: $mysql_port
user: $db_user
password: $db_pass
databases: $ccnet_db, $seafile_db, $seahub_db
Start/Stop:
Use $start_sh and $stop_sh to manage the server.
Initial admin:
Installer auto-creates an admin. See conf/initial_admin.txt
Notes:
- This follows CE 12.0 MySQL flow (setup-seafile-mysql.sh auto; 3 DBs).
- conf/.env includes JWT_PRIVATE_KEY (required by 12.x).
- seafevents (Pro) is disabled for CE.
""")
readme = readme_tpl.substitute(
seafile_version=args.seafile_version,
main_app_port=main_app_port,
cus_app_port=cus_app_port,
venv_dir=os.path.join(appdir, "venv"),
seafile_root=seafile_root,
conf_dir=conf_dir,
ccnet_dir=ccnet_dir,
data_dir=want_data,
server_domain=server_domain,
mysql_host=args.mysql_host,
mysql_port=args.mysql_port,
db_user=db_user_real,
db_pass=db_user_pass,
ccnet_db=ccnet_db,
seafile_db=seafile_db,
seahub_db=seahub_db,
start_sh=os.path.join(appdir, "start.sh"),
stop_sh=os.path.join(appdir, "stop.sh"),
)
create_file(os.path.join(appdir, "README_seafile.txt"), readme, 0o644)
# --------- Migrations & admin setup (using venv) ----------
server_latest = os.path.join(appdir, "seafile-server-latest")
seahub_sh = os.path.join(server_latest, "seahub.sh") # (may not be used directly below, but kept for reference)
# Prepare environment variables for Django manage.py commands
env_vars = dict(CMD_ENV)
env_vars.update({
"SEAFILE_CENTRAL_CONF_DIR": conf_dir,
"CCNET_CONF_DIR": ccnet_dir,
"SEAFILE_CONF_DIR": want_data,
"LANG": "en_US.UTF-8",
"LC_ALL": "en_US.UTF-8"
})
# Mark app as installed and send a completion notice (best-effort)
try:
api.post("/app/installed/", [{"id": args.app_uuid}])
api.post("/notice/create/", [{
"type": "D",
"content": f"Seafile app {appinfo['name']} ready. Seahub port {main_app_port}, seafhttp port {cus_app_port}. Admin in conf/initial_admin.txt"
}])
except SystemExit:
raise
except Exception as e:
logging.error(f"Failed to post install completion/notice: {e}")
logging.info("Seafile installation complete")
print("INSTALLATION OK")
if __name__ == "__main__":
try:
main()
except SystemExit:
raise
except Exception as e:
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s: %(message)s", force=True)
logging.exception(f"Unhandled error: {e}")
sys.exit(1)