diff --git a/apps/snipo/1.7.4/.env.sample b/apps/snipo/1.7.4/.env.sample new file mode 100644 index 000000000..b67d03e22 --- /dev/null +++ b/apps/snipo/1.7.4/.env.sample @@ -0,0 +1,15 @@ +CONTAINER_NAME=snipo +PANEL_APP_BIND_ADDRESS=127.0.0.1 +PANEL_APP_PORT_HTTP=8080 +SNIPO_MASTER_PASSWORD=replace-with-a-strong-unique-password +SNIPO_SESSION_SECRET=replace-with-a-random-session-secret +SNIPO_ENCRYPTION_SALT=replace-with-a-random-encryption-salt +SNIPO_SESSION_DURATION=168h +SNIPO_TRUST_PROXY=false +SNIPO_MAX_FILES_PER_SNIPPET=10 +SNIPO_ENABLE_PUBLIC_SNIPPETS=true +SNIPO_ENABLE_API_TOKENS=true +SNIPO_ENABLE_BACKUP_RESTORE=true +SNIPO_LOG_LEVEL=info +SNIPO_LOG_FORMAT=json +APP_DATA_DIR=./data diff --git a/apps/snipo/1.7.4/data.yml b/apps/snipo/1.7.4/data.yml new file mode 100644 index 000000000..336eec900 --- /dev/null +++ b/apps/snipo/1.7.4/data.yml @@ -0,0 +1,265 @@ +additionalProperties: + formFields: + - default: 127.0.0.1 + edit: true + envKey: PANEL_APP_BIND_ADDRESS + labelEn: Bind Address + labelZh: 绑定地址 + label: + en: Bind Address + zh: 绑定地址 + zh-Hant: 綁定位址 + ja: バインドアドレス + ko: 바인드 주소 + ru: Адрес привязки + ms: Alamat ikatan + pt-br: Endereco de vinculacao + required: true + type: text + - default: 8080 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: "" + edit: true + envKey: SNIPO_MASTER_PASSWORD + labelEn: Master Password + labelZh: 主密码 + label: + en: Master Password + zh: 主密码 + zh-Hant: 主密碼 + ja: マスターパスワード + ko: 마스터 비밀번호 + ru: Главный пароль + ms: Kata laluan induk + pt-br: Senha mestra + random: true + required: true + rule: paramComplexity + type: password + - default: "" + edit: false + envKey: SNIPO_SESSION_SECRET + labelEn: Session Secret (Auto-generated) + labelZh: 会话密钥(自动生成) + label: + en: Session Secret (Auto-generated) + zh: 会话密钥(自动生成) + zh-Hant: 工作階段密鑰(自動產生) + ja: セッションシークレット(自動生成) + ko: 세션 비밀 키(자동 생성) + ru: Секрет сеанса (создается автоматически) + ms: Rahsia sesi (dijana secara automatik) + pt-br: Segredo de sessao (gerado automaticamente) + random: true + required: true + type: password + - default: "" + edit: false + envKey: SNIPO_ENCRYPTION_SALT + labelEn: Encryption Salt (Auto-generated) + labelZh: 加密盐(自动生成) + label: + en: Encryption Salt (Auto-generated) + zh: 加密盐(自动生成) + zh-Hant: 加密鹽值(自動產生) + ja: 暗号化ソルト(自動生成) + ko: 암호화 솔트(자동 생성) + ru: Соль шифрования (создается автоматически) + ms: Garam penyulitan (dijana secara automatik) + pt-br: Sal de criptografia (gerado automaticamente) + random: true + required: true + type: password + - default: 168h + edit: true + envKey: SNIPO_SESSION_DURATION + labelEn: Session Duration + labelZh: 会话有效期 + label: + en: Session Duration + zh: 会话有效期 + zh-Hant: 工作階段有效期 + ja: セッション有効期間 + ko: 세션 유효 기간 + ru: Срок действия сеанса + ms: Tempoh sesi + pt-br: Duracao da sessao + required: true + type: text + - default: "false" + edit: true + envKey: SNIPO_TRUST_PROXY + labelEn: Trust Reverse Proxy Headers + labelZh: 信任反向代理请求头 + label: + en: Trust Reverse Proxy Headers + zh: 信任反向代理请求头 + zh-Hant: 信任反向代理請求標頭 + ja: リバースプロキシヘッダーを信頼 + ko: 리버스 프록시 헤더 신뢰 + ru: Доверять заголовкам обратного прокси + ms: Percayai pengepala proksi songsang + pt-br: Confiar nos cabecalhos do proxy reverso + required: true + type: select + values: + - label: "false" + value: "false" + - label: "true" + value: "true" + - default: 10 + edit: true + envKey: SNIPO_MAX_FILES_PER_SNIPPET + labelEn: Maximum Files per Snippet + labelZh: 每个片段的最大文件数 + label: + en: Maximum Files per Snippet + zh: 每个片段的最大文件数 + zh-Hant: 每個片段的最大檔案數 + ja: スニペットごとの最大ファイル数 + ko: 코드 조각당 최대 파일 수 + ru: Максимум файлов в одном фрагменте + ms: Fail maksimum bagi setiap cebisan + pt-br: Maximo de arquivos por trecho + required: true + type: number + - default: "true" + edit: true + envKey: SNIPO_ENABLE_PUBLIC_SNIPPETS + labelEn: Enable Public Snippets + labelZh: 启用公开片段 + label: + en: Enable Public Snippets + zh: 启用公开片段 + zh-Hant: 啟用公開片段 + ja: 公開スニペットを有効化 + ko: 공개 코드 조각 활성화 + ru: Включить публичные фрагменты + ms: Dayakan cebisan awam + pt-br: Ativar trechos publicos + required: true + type: select + values: + - label: "true" + value: "true" + - label: "false" + value: "false" + - default: "true" + edit: true + envKey: SNIPO_ENABLE_API_TOKENS + labelEn: Enable API Tokens + labelZh: 启用 API Token + label: + en: Enable API Tokens + zh: 启用 API Token + zh-Hant: 啟用 API Token + ja: API トークンを有効化 + ko: API 토큰 활성화 + ru: Включить токены API + ms: Dayakan token API + pt-br: Ativar tokens de API + required: true + type: select + values: + - label: "true" + value: "true" + - label: "false" + value: "false" + - default: "true" + edit: true + envKey: SNIPO_ENABLE_BACKUP_RESTORE + labelEn: Enable Backup and Restore + labelZh: 启用备份与恢复 + label: + en: Enable Backup and Restore + zh: 启用备份与恢复 + zh-Hant: 啟用備份與還原 + ja: バックアップと復元を有効化 + ko: 백업 및 복원 활성화 + ru: Включить резервное копирование и восстановление + ms: Dayakan sandaran dan pemulihan + pt-br: Ativar backup e restauracao + required: true + type: select + values: + - label: "true" + value: "true" + - label: "false" + value: "false" + - default: info + edit: true + envKey: SNIPO_LOG_LEVEL + labelEn: Log Level + labelZh: 日志级别 + label: + en: Log Level + zh: 日志级别 + zh-Hant: 日誌級別 + ja: ログレベル + ko: 로그 수준 + ru: Уровень журнала + ms: Tahap log + pt-br: Nivel de log + required: true + type: select + values: + - label: info + value: info + - label: warn + value: warn + - label: error + value: error + - label: debug + value: debug + - default: json + edit: true + envKey: SNIPO_LOG_FORMAT + labelEn: Log Format + labelZh: 日志格式 + label: + en: Log Format + zh: 日志格式 + zh-Hant: 日誌格式 + ja: ログ形式 + ko: 로그 형식 + ru: Формат журнала + ms: Format log + pt-br: Formato de log + required: true + type: select + values: + - label: json + value: json + - label: text + value: text + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori data + pt-br: Diretorio de dados + required: true + type: text diff --git a/apps/snipo/1.7.4/docker-compose.yml b/apps/snipo/1.7.4/docker-compose.yml new file mode 100644 index 000000000..62a8bd460 --- /dev/null +++ b/apps/snipo/1.7.4/docker-compose.yml @@ -0,0 +1,56 @@ +services: + snipo: + image: "ghcr.io/mohamedelashri/snipo:1.7.4@sha256:bd9d1fa2245ad2bcdaaac0e34df3094f2005e7397de3e42c9aeb0ae9ec765e9f" + container_name: ${CONTAINER_NAME} + restart: unless-stopped + user: "1000:1000" + networks: + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:8080" + environment: + - SNIPO_MASTER_PASSWORD=${SNIPO_MASTER_PASSWORD} + - SNIPO_SESSION_SECRET=${SNIPO_SESSION_SECRET} + - SNIPO_ENCRYPTION_SALT=${SNIPO_ENCRYPTION_SALT} + - SNIPO_SESSION_DURATION=${SNIPO_SESSION_DURATION} + - SNIPO_HOST=0.0.0.0 + - SNIPO_PORT=8080 + - SNIPO_TRUST_PROXY=${SNIPO_TRUST_PROXY} + - SNIPO_MAX_FILES_PER_SNIPPET=${SNIPO_MAX_FILES_PER_SNIPPET} + - SNIPO_DB_PATH=/data/snipo.db + - SNIPO_DB_MAX_CONNS=1 + - SNIPO_DB_BUSY_TIMEOUT=5000 + - SNIPO_DB_JOURNAL=WAL + - SNIPO_DB_SYNC=NORMAL + - SNIPO_DB_MMAP_SIZE=67108864 + - SNIPO_DB_CACHE_SIZE=-1000 + - SNIPO_RATE_LIMIT=100 + - SNIPO_RATE_LIMIT_READ=1000 + - SNIPO_RATE_LIMIT_WRITE=500 + - SNIPO_RATE_LIMIT_ADMIN=100 + - SNIPO_ENABLE_PUBLIC_SNIPPETS=${SNIPO_ENABLE_PUBLIC_SNIPPETS} + - SNIPO_ENABLE_API_TOKENS=${SNIPO_ENABLE_API_TOKENS} + - SNIPO_ENABLE_BACKUP_RESTORE=${SNIPO_ENABLE_BACKUP_RESTORE} + - SNIPO_LOG_LEVEL=${SNIPO_LOG_LEVEL} + - SNIPO_LOG_FORMAT=${SNIPO_LOG_FORMAT} + volumes: + - "${APP_DATA_DIR}:/data" + read_only: true + tmpfs: + - /tmp:rw,nosuid,nodev,noexec,size=16m,uid=1000,gid=1000,mode=1777 + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "/snipo", "health"] + interval: 30s + timeout: 5s + retries: 8 + start_period: 10s + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/snipo/1.7.4/scripts/init.sh b/apps/snipo/1.7.4/scripts/init.sh new file mode 100755 index 000000000..8bfe31a89 --- /dev/null +++ b/apps/snipo/1.7.4/scripts/init.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +APP_ROOT_DIR="$(dirname "$ROOT_DIR")" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +strip_matching_quotes() { + local value="$1" + + if [[ ${#value} -ge 2 ]]; then + if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then + value="${value:1:${#value}-2}" + elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then + value="${value:1:${#value}-2}" + fi + fi + printf '%s\n' "$value" +} + +read_env_value() { + local key="$1" + local value="" + + if [[ -f "$ENV_FILE" ]]; then + value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- || true)" + fi + value="${value%$'\r'}" + strip_matching_quotes "$value" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + + temp_file="$(mktemp "${ENV_FILE}.tmp.XXXXXX")" + awk -v key="$key" -v value="$value" ' + BEGIN { updated = 0 } + $0 ~ ("^" key "=") { + if (!updated) { + print key "=" value + updated = 1 + } + next + } + { print } + END { + if (!updated) print key "=" value + } + ' "$ENV_FILE" >"$temp_file" + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +path_is_dotenv_safe() { + local value="$1" + + case "$value" in + *$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'"*) return 1 ;; + *) return 0 ;; + esac +} + +[[ -f "$ENV_FILE" ]] || fail "Environment file not found: ${ENV_FILE}" +[[ ! -L "$ENV_FILE" ]] || fail "Environment file must not be a symbolic link" +[[ "$(id -u)" -eq 0 ]] || fail "Snipo init must run as root" + +if [[ ${APP_DATA_DIR+x} ]]; then + DATA_DIR_RAW="$APP_DATA_DIR" +else + DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)" +fi +DATA_DIR_RAW="$(strip_matching_quotes "${DATA_DIR_RAW:-./data}")" + +[[ -n "$DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty" +path_is_dotenv_safe "$DATA_DIR_RAW" || fail "APP_DATA_DIR contains unsupported dotenv characters" + +if [[ "$DATA_DIR_RAW" = /* ]]; then + [[ ! -L "$DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be a symbolic link" + DATA_DIR_ABS="$(realpath -m -- "$DATA_DIR_RAW")" + [[ "$DATA_DIR_ABS" != "/" ]] || fail "APP_DATA_DIR must not be the filesystem root" +else + CONTAINER_NAME_VALUE="$(read_env_value CONTAINER_NAME)" + [[ "$CONTAINER_NAME_VALUE" =~ ^[A-Za-z0-9._-]+$ ]] || \ + fail "CONTAINER_NAME contains unsupported characters" + + RETAINED_ROOT="${APP_ROOT_DIR}/retained-data" + [[ ! -L "$RETAINED_ROOT" ]] || fail "The retained data root must not be a symbolic link" + RETAINED_INSTANCE_ROOT="${RETAINED_ROOT}/${CONTAINER_NAME_VALUE}" + [[ ! -L "$RETAINED_INSTANCE_ROOT" ]] || fail "The retained instance root must not be a symbolic link" + DATA_DIR_PATH="${RETAINED_INSTANCE_ROOT}/${DATA_DIR_RAW#./}" + [[ ! -L "$DATA_DIR_PATH" ]] || fail "APP_DATA_DIR must not be a symbolic link" + RETAINED_INSTANCE_ABS="$(realpath -m -- "$RETAINED_INSTANCE_ROOT")" + DATA_DIR_ABS="$(realpath -m -- "$DATA_DIR_PATH")" + case "$DATA_DIR_ABS" in + "${RETAINED_INSTANCE_ABS}"/*) ;; + *) fail "Relative APP_DATA_DIR must remain inside the isolated retained-data directory" ;; + esac + + set_env_value APP_DATA_DIR "\"${DATA_DIR_ABS}\"" +fi + +if [[ -e "$DATA_DIR_ABS" && ! -d "$DATA_DIR_ABS" ]]; then + fail "APP_DATA_DIR must be a directory" +fi +if [[ ! -e "$DATA_DIR_ABS" ]]; then + install -d -m 0750 -- "$DATA_DIR_ABS" + chown 1000:1000 -- "$DATA_DIR_ABS" +fi + +[[ "$(stat -c '%u:%g' "$DATA_DIR_ABS")" == "1000:1000" ]] || \ + fail "APP_DATA_DIR must be owned by UID/GID 1000:1000" diff --git a/apps/snipo/1.7.4/scripts/uninstall.sh b/apps/snipo/1.7.4/scripts/uninstall.sh new file mode 100755 index 000000000..9365d9925 --- /dev/null +++ b/apps/snipo/1.7.4/scripts/uninstall.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Persistent data is intentionally retained for recovery or reinstallation. +exit 0 diff --git a/apps/snipo/1.7.4/scripts/upgrade.sh b/apps/snipo/1.7.4/scripts/upgrade.sh new file mode 100755 index 000000000..13092bde7 --- /dev/null +++ b/apps/snipo/1.7.4/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Snipo applies embedded SQLite migrations when the new container starts. +exit 0 diff --git a/apps/snipo/README.md b/apps/snipo/README.md new file mode 100644 index 000000000..899689e21 --- /dev/null +++ b/apps/snipo/README.md @@ -0,0 +1,51 @@ +# Snipo + +## 产品介绍 + +Snipo 是一个面向单用户的轻量级代码片段管理器,支持在一个自托管实例中组织、搜索和分享多文件代码片段。 + +## 主要功能 + +- 多文件代码片段、语法高亮、文件夹、标签、搜索和历史记录 +- 公开链接、软删除、过期时间和备份恢复 +- 可按权限创建读、写和管理 API Token +- 使用内置 SQLite 数据库,无需外部数据库 + +## 访问说明 + +安装时设置主密码,随后通过 `http://<服务器 IP>:<端口>` 访问并登录。Snipo 只有一个共享的主密码和数据库,不提供多用户或租户隔离。默认绑定 `127.0.0.1`;远程访问应通过 1Panel 的 HTTPS 反向代理发布。 + +## 数据与安全 + +- 数据库、自动生成的加密盐和备份数据保存在 `APP_DATA_DIR` 中。相对路径会在安装时解析到应用级 `retained-data` 目录并按容器名隔离,卸载实例后仍会保留;绝对路径保持不变。升级或迁移前应备份 SQLite 数据库、WAL/SHM 文件和 `.encryption_salt`。 +- 容器以 UID/GID `1000:1000` 运行,根文件系统只读,丢弃全部 Linux capabilities,并只提供一个小型可写 `/tmp`。 +- 默认启用公开片段、API Token 和备份恢复功能,可在安装表单中关闭不需要的功能。 +- 公开片段可由持有链接的任何人访问。对公网部署时应启用 HTTPS,并避免绕过反向代理直接访问应用端口。 +- Snipo 启动时自动执行内置数据库迁移。升级后应检查 `/health` 并验证常用片段的读写和持久化。 + +## Introduction + +Snipo is a lightweight, single-user snippet manager for organizing, searching, and sharing multi-file code snippets in one self-hosted instance. + +## Features + +- Multi-file snippets, syntax highlighting, folders, tags, search, and history +- Public links, soft deletion, expiration, and backup/restore +- Read, write, and administrative API tokens +- Embedded SQLite storage without an external database + +## Usage And Security + +- Set the master password during installation, then sign in at `http://:`. Snipo has one shared password and database; it does not provide multi-user isolation. +- The service binds to `127.0.0.1` by default. Publish it remotely through a 1Panel HTTPS reverse proxy. +- `APP_DATA_DIR` stores the SQLite database, generated encryption salt, and backups. Relative paths are resolved into a per-container directory under the app-level `retained-data` directory so uninstalling an instance preserves them; absolute paths remain unchanged. +- The container runs as UID/GID `1000:1000`, uses a read-only root filesystem, drops all Linux capabilities, and receives only a small writable `/tmp`. +- Public snippets are accessible to anyone with their link. Review the public-sharing, API-token, and backup switches before exposing the service. + +## References + +- Project: +- Stable release: +- Deployment guide: +- Security model: +- License: (AGPL-3.0) diff --git a/apps/snipo/data.yml b/apps/snipo/data.yml new file mode 100644 index 000000000..7fa94aadb --- /dev/null +++ b/apps/snipo/data.yml @@ -0,0 +1,31 @@ +name: Snipo +tags: + - 工具 +title: 轻量级单用户代码片段管理器 +description: 轻量级单用户代码片段管理器 +additionalProperties: + key: snipo + name: Snipo + tags: + - Tool + shortDescZh: 轻量级单用户代码片段管理器 + shortDescEn: Lightweight single-user snippet manager + description: + en: Lightweight single-user snippet manager + zh: 轻量级单用户代码片段管理器 + zh-Hant: 輕量級單一使用者程式碼片段管理器 + ja: 軽量なシングルユーザー向けスニペット管理ツール + ko: 가벼운 단일 사용자 코드 조각 관리자 + ru: Легковесный менеджер фрагментов кода для одного пользователя + ms: Pengurus cebisan kod ringan untuk seorang pengguna + pt-br: Gerenciador leve de trechos de codigo para um unico usuario + type: website + crossVersionUpdate: true + limit: 0 + recommend: 0 + website: https://github.com/MohamedElashri/snipo + github: https://github.com/MohamedElashri/snipo + document: https://github.com/MohamedElashri/snipo/blob/main/docs/deployment.md + architectures: + - amd64 + - arm64 diff --git a/apps/snipo/latest/.env.sample b/apps/snipo/latest/.env.sample new file mode 100644 index 000000000..b67d03e22 --- /dev/null +++ b/apps/snipo/latest/.env.sample @@ -0,0 +1,15 @@ +CONTAINER_NAME=snipo +PANEL_APP_BIND_ADDRESS=127.0.0.1 +PANEL_APP_PORT_HTTP=8080 +SNIPO_MASTER_PASSWORD=replace-with-a-strong-unique-password +SNIPO_SESSION_SECRET=replace-with-a-random-session-secret +SNIPO_ENCRYPTION_SALT=replace-with-a-random-encryption-salt +SNIPO_SESSION_DURATION=168h +SNIPO_TRUST_PROXY=false +SNIPO_MAX_FILES_PER_SNIPPET=10 +SNIPO_ENABLE_PUBLIC_SNIPPETS=true +SNIPO_ENABLE_API_TOKENS=true +SNIPO_ENABLE_BACKUP_RESTORE=true +SNIPO_LOG_LEVEL=info +SNIPO_LOG_FORMAT=json +APP_DATA_DIR=./data diff --git a/apps/snipo/latest/data.yml b/apps/snipo/latest/data.yml new file mode 100644 index 000000000..336eec900 --- /dev/null +++ b/apps/snipo/latest/data.yml @@ -0,0 +1,265 @@ +additionalProperties: + formFields: + - default: 127.0.0.1 + edit: true + envKey: PANEL_APP_BIND_ADDRESS + labelEn: Bind Address + labelZh: 绑定地址 + label: + en: Bind Address + zh: 绑定地址 + zh-Hant: 綁定位址 + ja: バインドアドレス + ko: 바인드 주소 + ru: Адрес привязки + ms: Alamat ikatan + pt-br: Endereco de vinculacao + required: true + type: text + - default: 8080 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: "" + edit: true + envKey: SNIPO_MASTER_PASSWORD + labelEn: Master Password + labelZh: 主密码 + label: + en: Master Password + zh: 主密码 + zh-Hant: 主密碼 + ja: マスターパスワード + ko: 마스터 비밀번호 + ru: Главный пароль + ms: Kata laluan induk + pt-br: Senha mestra + random: true + required: true + rule: paramComplexity + type: password + - default: "" + edit: false + envKey: SNIPO_SESSION_SECRET + labelEn: Session Secret (Auto-generated) + labelZh: 会话密钥(自动生成) + label: + en: Session Secret (Auto-generated) + zh: 会话密钥(自动生成) + zh-Hant: 工作階段密鑰(自動產生) + ja: セッションシークレット(自動生成) + ko: 세션 비밀 키(자동 생성) + ru: Секрет сеанса (создается автоматически) + ms: Rahsia sesi (dijana secara automatik) + pt-br: Segredo de sessao (gerado automaticamente) + random: true + required: true + type: password + - default: "" + edit: false + envKey: SNIPO_ENCRYPTION_SALT + labelEn: Encryption Salt (Auto-generated) + labelZh: 加密盐(自动生成) + label: + en: Encryption Salt (Auto-generated) + zh: 加密盐(自动生成) + zh-Hant: 加密鹽值(自動產生) + ja: 暗号化ソルト(自動生成) + ko: 암호화 솔트(자동 생성) + ru: Соль шифрования (создается автоматически) + ms: Garam penyulitan (dijana secara automatik) + pt-br: Sal de criptografia (gerado automaticamente) + random: true + required: true + type: password + - default: 168h + edit: true + envKey: SNIPO_SESSION_DURATION + labelEn: Session Duration + labelZh: 会话有效期 + label: + en: Session Duration + zh: 会话有效期 + zh-Hant: 工作階段有效期 + ja: セッション有効期間 + ko: 세션 유효 기간 + ru: Срок действия сеанса + ms: Tempoh sesi + pt-br: Duracao da sessao + required: true + type: text + - default: "false" + edit: true + envKey: SNIPO_TRUST_PROXY + labelEn: Trust Reverse Proxy Headers + labelZh: 信任反向代理请求头 + label: + en: Trust Reverse Proxy Headers + zh: 信任反向代理请求头 + zh-Hant: 信任反向代理請求標頭 + ja: リバースプロキシヘッダーを信頼 + ko: 리버스 프록시 헤더 신뢰 + ru: Доверять заголовкам обратного прокси + ms: Percayai pengepala proksi songsang + pt-br: Confiar nos cabecalhos do proxy reverso + required: true + type: select + values: + - label: "false" + value: "false" + - label: "true" + value: "true" + - default: 10 + edit: true + envKey: SNIPO_MAX_FILES_PER_SNIPPET + labelEn: Maximum Files per Snippet + labelZh: 每个片段的最大文件数 + label: + en: Maximum Files per Snippet + zh: 每个片段的最大文件数 + zh-Hant: 每個片段的最大檔案數 + ja: スニペットごとの最大ファイル数 + ko: 코드 조각당 최대 파일 수 + ru: Максимум файлов в одном фрагменте + ms: Fail maksimum bagi setiap cebisan + pt-br: Maximo de arquivos por trecho + required: true + type: number + - default: "true" + edit: true + envKey: SNIPO_ENABLE_PUBLIC_SNIPPETS + labelEn: Enable Public Snippets + labelZh: 启用公开片段 + label: + en: Enable Public Snippets + zh: 启用公开片段 + zh-Hant: 啟用公開片段 + ja: 公開スニペットを有効化 + ko: 공개 코드 조각 활성화 + ru: Включить публичные фрагменты + ms: Dayakan cebisan awam + pt-br: Ativar trechos publicos + required: true + type: select + values: + - label: "true" + value: "true" + - label: "false" + value: "false" + - default: "true" + edit: true + envKey: SNIPO_ENABLE_API_TOKENS + labelEn: Enable API Tokens + labelZh: 启用 API Token + label: + en: Enable API Tokens + zh: 启用 API Token + zh-Hant: 啟用 API Token + ja: API トークンを有効化 + ko: API 토큰 활성화 + ru: Включить токены API + ms: Dayakan token API + pt-br: Ativar tokens de API + required: true + type: select + values: + - label: "true" + value: "true" + - label: "false" + value: "false" + - default: "true" + edit: true + envKey: SNIPO_ENABLE_BACKUP_RESTORE + labelEn: Enable Backup and Restore + labelZh: 启用备份与恢复 + label: + en: Enable Backup and Restore + zh: 启用备份与恢复 + zh-Hant: 啟用備份與還原 + ja: バックアップと復元を有効化 + ko: 백업 및 복원 활성화 + ru: Включить резервное копирование и восстановление + ms: Dayakan sandaran dan pemulihan + pt-br: Ativar backup e restauracao + required: true + type: select + values: + - label: "true" + value: "true" + - label: "false" + value: "false" + - default: info + edit: true + envKey: SNIPO_LOG_LEVEL + labelEn: Log Level + labelZh: 日志级别 + label: + en: Log Level + zh: 日志级别 + zh-Hant: 日誌級別 + ja: ログレベル + ko: 로그 수준 + ru: Уровень журнала + ms: Tahap log + pt-br: Nivel de log + required: true + type: select + values: + - label: info + value: info + - label: warn + value: warn + - label: error + value: error + - label: debug + value: debug + - default: json + edit: true + envKey: SNIPO_LOG_FORMAT + labelEn: Log Format + labelZh: 日志格式 + label: + en: Log Format + zh: 日志格式 + zh-Hant: 日誌格式 + ja: ログ形式 + ko: 로그 형식 + ru: Формат журнала + ms: Format log + pt-br: Formato de log + required: true + type: select + values: + - label: json + value: json + - label: text + value: text + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori data + pt-br: Diretorio de dados + required: true + type: text diff --git a/apps/snipo/latest/docker-compose.yml b/apps/snipo/latest/docker-compose.yml new file mode 100644 index 000000000..6a54228c1 --- /dev/null +++ b/apps/snipo/latest/docker-compose.yml @@ -0,0 +1,56 @@ +services: + snipo: + image: "ghcr.io/mohamedelashri/snipo:latest" + container_name: ${CONTAINER_NAME} + restart: unless-stopped + user: "1000:1000" + networks: + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:8080" + environment: + - SNIPO_MASTER_PASSWORD=${SNIPO_MASTER_PASSWORD} + - SNIPO_SESSION_SECRET=${SNIPO_SESSION_SECRET} + - SNIPO_ENCRYPTION_SALT=${SNIPO_ENCRYPTION_SALT} + - SNIPO_SESSION_DURATION=${SNIPO_SESSION_DURATION} + - SNIPO_HOST=0.0.0.0 + - SNIPO_PORT=8080 + - SNIPO_TRUST_PROXY=${SNIPO_TRUST_PROXY} + - SNIPO_MAX_FILES_PER_SNIPPET=${SNIPO_MAX_FILES_PER_SNIPPET} + - SNIPO_DB_PATH=/data/snipo.db + - SNIPO_DB_MAX_CONNS=1 + - SNIPO_DB_BUSY_TIMEOUT=5000 + - SNIPO_DB_JOURNAL=WAL + - SNIPO_DB_SYNC=NORMAL + - SNIPO_DB_MMAP_SIZE=67108864 + - SNIPO_DB_CACHE_SIZE=-1000 + - SNIPO_RATE_LIMIT=100 + - SNIPO_RATE_LIMIT_READ=1000 + - SNIPO_RATE_LIMIT_WRITE=500 + - SNIPO_RATE_LIMIT_ADMIN=100 + - SNIPO_ENABLE_PUBLIC_SNIPPETS=${SNIPO_ENABLE_PUBLIC_SNIPPETS} + - SNIPO_ENABLE_API_TOKENS=${SNIPO_ENABLE_API_TOKENS} + - SNIPO_ENABLE_BACKUP_RESTORE=${SNIPO_ENABLE_BACKUP_RESTORE} + - SNIPO_LOG_LEVEL=${SNIPO_LOG_LEVEL} + - SNIPO_LOG_FORMAT=${SNIPO_LOG_FORMAT} + volumes: + - "${APP_DATA_DIR}:/data" + read_only: true + tmpfs: + - /tmp:rw,nosuid,nodev,noexec,size=16m,uid=1000,gid=1000,mode=1777 + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "/snipo", "health"] + interval: 30s + timeout: 5s + retries: 8 + start_period: 10s + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/snipo/latest/scripts/init.sh b/apps/snipo/latest/scripts/init.sh new file mode 100755 index 000000000..8bfe31a89 --- /dev/null +++ b/apps/snipo/latest/scripts/init.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +APP_ROOT_DIR="$(dirname "$ROOT_DIR")" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +strip_matching_quotes() { + local value="$1" + + if [[ ${#value} -ge 2 ]]; then + if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then + value="${value:1:${#value}-2}" + elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then + value="${value:1:${#value}-2}" + fi + fi + printf '%s\n' "$value" +} + +read_env_value() { + local key="$1" + local value="" + + if [[ -f "$ENV_FILE" ]]; then + value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- || true)" + fi + value="${value%$'\r'}" + strip_matching_quotes "$value" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + + temp_file="$(mktemp "${ENV_FILE}.tmp.XXXXXX")" + awk -v key="$key" -v value="$value" ' + BEGIN { updated = 0 } + $0 ~ ("^" key "=") { + if (!updated) { + print key "=" value + updated = 1 + } + next + } + { print } + END { + if (!updated) print key "=" value + } + ' "$ENV_FILE" >"$temp_file" + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +path_is_dotenv_safe() { + local value="$1" + + case "$value" in + *$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'"*) return 1 ;; + *) return 0 ;; + esac +} + +[[ -f "$ENV_FILE" ]] || fail "Environment file not found: ${ENV_FILE}" +[[ ! -L "$ENV_FILE" ]] || fail "Environment file must not be a symbolic link" +[[ "$(id -u)" -eq 0 ]] || fail "Snipo init must run as root" + +if [[ ${APP_DATA_DIR+x} ]]; then + DATA_DIR_RAW="$APP_DATA_DIR" +else + DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)" +fi +DATA_DIR_RAW="$(strip_matching_quotes "${DATA_DIR_RAW:-./data}")" + +[[ -n "$DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty" +path_is_dotenv_safe "$DATA_DIR_RAW" || fail "APP_DATA_DIR contains unsupported dotenv characters" + +if [[ "$DATA_DIR_RAW" = /* ]]; then + [[ ! -L "$DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be a symbolic link" + DATA_DIR_ABS="$(realpath -m -- "$DATA_DIR_RAW")" + [[ "$DATA_DIR_ABS" != "/" ]] || fail "APP_DATA_DIR must not be the filesystem root" +else + CONTAINER_NAME_VALUE="$(read_env_value CONTAINER_NAME)" + [[ "$CONTAINER_NAME_VALUE" =~ ^[A-Za-z0-9._-]+$ ]] || \ + fail "CONTAINER_NAME contains unsupported characters" + + RETAINED_ROOT="${APP_ROOT_DIR}/retained-data" + [[ ! -L "$RETAINED_ROOT" ]] || fail "The retained data root must not be a symbolic link" + RETAINED_INSTANCE_ROOT="${RETAINED_ROOT}/${CONTAINER_NAME_VALUE}" + [[ ! -L "$RETAINED_INSTANCE_ROOT" ]] || fail "The retained instance root must not be a symbolic link" + DATA_DIR_PATH="${RETAINED_INSTANCE_ROOT}/${DATA_DIR_RAW#./}" + [[ ! -L "$DATA_DIR_PATH" ]] || fail "APP_DATA_DIR must not be a symbolic link" + RETAINED_INSTANCE_ABS="$(realpath -m -- "$RETAINED_INSTANCE_ROOT")" + DATA_DIR_ABS="$(realpath -m -- "$DATA_DIR_PATH")" + case "$DATA_DIR_ABS" in + "${RETAINED_INSTANCE_ABS}"/*) ;; + *) fail "Relative APP_DATA_DIR must remain inside the isolated retained-data directory" ;; + esac + + set_env_value APP_DATA_DIR "\"${DATA_DIR_ABS}\"" +fi + +if [[ -e "$DATA_DIR_ABS" && ! -d "$DATA_DIR_ABS" ]]; then + fail "APP_DATA_DIR must be a directory" +fi +if [[ ! -e "$DATA_DIR_ABS" ]]; then + install -d -m 0750 -- "$DATA_DIR_ABS" + chown 1000:1000 -- "$DATA_DIR_ABS" +fi + +[[ "$(stat -c '%u:%g' "$DATA_DIR_ABS")" == "1000:1000" ]] || \ + fail "APP_DATA_DIR must be owned by UID/GID 1000:1000" diff --git a/apps/snipo/latest/scripts/uninstall.sh b/apps/snipo/latest/scripts/uninstall.sh new file mode 100755 index 000000000..9365d9925 --- /dev/null +++ b/apps/snipo/latest/scripts/uninstall.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Persistent data is intentionally retained for recovery or reinstallation. +exit 0 diff --git a/apps/snipo/latest/scripts/upgrade.sh b/apps/snipo/latest/scripts/upgrade.sh new file mode 100755 index 000000000..13092bde7 --- /dev/null +++ b/apps/snipo/latest/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Snipo applies embedded SQLite migrations when the new container starts. +exit 0 diff --git a/apps/snipo/logo-LICENSE.txt b/apps/snipo/logo-LICENSE.txt new file mode 100644 index 000000000..3f68f8c52 --- /dev/null +++ b/apps/snipo/logo-LICENSE.txt @@ -0,0 +1,9 @@ +Snipo logo + +Source: https://github.com/MohamedElashri/snipo/blob/c5eab947cce8728c2a431d2ed3426d452d14c3bf/assets/logo/icon-256.png +Source commit: c5eab947cce8728c2a431d2ed3426d452d14c3bf +Source SHA-256: dae9ce2b27caa1c1dbbc27669773d8f9d229a41f5ce2550f08f77c52c7331edb +License: GNU Affero General Public License v3.0 (AGPL-3.0-only) +License text: https://github.com/MohamedElashri/snipo/blob/c5eab947cce8728c2a431d2ed3426d452d14c3bf/LICENSE + +The store asset is a proportional rendering of the official project icon. diff --git a/apps/snipo/logo.png b/apps/snipo/logo.png new file mode 100644 index 000000000..d8dd565cb Binary files /dev/null and b/apps/snipo/logo.png differ