From 0132be7586bb5eff9a86bb799c47a7aa99155149 Mon Sep 17 00:00:00 2001 From: cheney Date: Wed, 17 Jun 2026 09:13:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20docker=20runsh=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kit/src/docker/index.js | 261 ++++++++++++++++++++++++ kit/src/index.js | 7 + kit/src/kitfunction.js | 54 ++++- kit/src/linux/kitfunction.js | 55 +++++- kit/src/windows/kitfunction.js | 60 +++++- kit/test/docker/restore-run.test.js | 21 ++ kit/test/docker/testcase-inspect.json | 272 ++++++++++++++++++++++++++ kit/test/docker/testcase-run.sh | 25 +++ kit/test/remote_test.js | 27 +++ 9 files changed, 771 insertions(+), 11 deletions(-) create mode 100644 kit/src/docker/index.js create mode 100644 kit/test/docker/restore-run.test.js create mode 100644 kit/test/docker/testcase-inspect.json create mode 100644 kit/test/docker/testcase-run.sh diff --git a/kit/src/docker/index.js b/kit/src/docker/index.js new file mode 100644 index 0000000..b2abfc6 --- /dev/null +++ b/kit/src/docker/index.js @@ -0,0 +1,261 @@ +#!/usr/bin/env node +const { execFileSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +function exitError(message, code = 1) { + console.error("ERROR: " + message); + process.exit(code); +} + +function run(command, args) { + return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); +} + +function ensureDocker() { + try { + run("docker", ["version", "--format", "{{.Server.Version}}"]); + } catch (e) { + exitError("docker command is not executable, or Docker daemon is unavailable"); + } +} + +function inspectContainer(container) { + try { + const text = run("docker", ["inspect", container]); + const data = JSON.parse(text); + if (!Array.isArray(data) || data.length === 0) { + exitError("container not found: " + container); + } + return data[0]; + } catch (e) { + if (e instanceof SyntaxError) { + exitError("docker inspect output is not valid JSON"); + } + exitError("docker inspect failed: " + container); + } +} + +function quoteSh(value) { + if (value === undefined || value === null) { + return "''"; + } + const text = String(value); + if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(text)) { + return text; + } + return "'" + text.replace(/'/g, "'\\''") + "'"; +} + +function quoteMountSpec(value) { + const text = String(value); + if (text.startsWith("$(pwd)")) { + const rest = text.slice("$(pwd)".length).replace(/(["\\`$])/g, "\\$1"); + return '"$(pwd)' + rest + '"'; + } + return quoteSh(text); +} + +function normalizeContainerName(name) { + return (name || "container").replace(/^\/+/, "") || "container"; +} + +function isLikelyProjectPath(source) { + if (!source) return false; + const normalized = source.replace(/\\/g, "/").replace(/\/+$/, ""); + if (normalized === "/") return false; + if (/^[A-Za-z]:$/i.test(normalized)) return false; + + const specialPrefixes = [ + "/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", "/proc", "/run", "/sbin", "/sys", "/tmp", "/usr", "/var", + "/opt/docker", "/var/lib/docker", "/var/run/docker.sock", + "C:/Program Files", "C:/Program Files (x86)", "C:/ProgramData", "C:/Windows" + ]; + return !specialPrefixes.some(prefix => normalized === prefix || normalized.startsWith(prefix + "/")); +} + +function relativeToPwdIfProject(source) { + if (!isLikelyProjectPath(source)) { + return source; + } + + const normalized = source.replace(/\\/g, "/").replace(/\/+$/, ""); + const base = path.posix.basename(normalized); + if (!base || base === "." || base === "/") { + return source; + } + return "$(pwd)/" + base; +} + +function collectEnv(config) { + const env = Array.isArray(config.Env) ? config.Env : []; + return env + .filter(item => typeof item === "string" && item.length > 0) + .map(item => " -e " + quoteSh(item) + " \\"); +} + +function collectPorts(networkSettings, hostConfig) { + const lines = []; + const seen = new Set(); + const ports = Object.assign({}, networkSettings && networkSettings.Ports ? networkSettings.Ports : {}); + const bindings = hostConfig && hostConfig.PortBindings ? hostConfig.PortBindings : {}; + for (const containerPort of Object.keys(bindings)) { + if (!ports[containerPort]) ports[containerPort] = bindings[containerPort]; + } + + for (const containerPort of Object.keys(ports).sort()) { + const entries = ports[containerPort]; + if (!Array.isArray(entries)) continue; + for (const entry of entries) { + if (!entry || !entry.HostPort) continue; + const hostIp = entry.HostIp && entry.HostIp !== "0.0.0.0" && entry.HostIp !== "::" ? entry.HostIp + ":" : ""; + const spec = hostIp + entry.HostPort + ":" + containerPort; + if (seen.has(spec)) continue; + seen.add(spec); + lines.push(" -p " + quoteSh(spec) + " \\"); + } + } + return lines; +} + +function collectMounts(info) { + const mounts = Array.isArray(info.Mounts) ? info.Mounts : []; + const lines = []; + for (const mount of mounts) { + if (!mount || !mount.Destination) continue; + if (mount.Type === "volume" && mount.Name) { + let spec = mount.Name + ":" + mount.Destination; + if (mount.RW === false) spec += ":ro"; + lines.push(" -v " + quoteMountSpec(spec) + " \\"); + continue; + } + if (mount.Source) { + let spec = relativeToPwdIfProject(mount.Source) + ":" + mount.Destination; + if (mount.RW === false) spec += ":ro"; + lines.push(" -v " + quoteMountSpec(spec) + " \\"); + } + } + return lines; +} + +function collectNetworks(info) { + const hostConfig = info.HostConfig || {}; + const networkMode = hostConfig.NetworkMode; + const lines = []; + if (networkMode && networkMode !== "default" && networkMode !== "bridge") { + lines.push(" --network " + quoteSh(networkMode) + " \\"); + return lines; + } + + const networkSettings = info.NetworkSettings || {}; + const networks = networkSettings.Networks ? Object.keys(networkSettings.Networks) : []; + if (networks.length === 1 && networks[0] !== "bridge") { + lines.push(" --network " + quoteSh(networks[0]) + " \\"); + } + return lines; +} + +function collectSimpleOptions(info) { + const config = info.Config || {}; + const hostConfig = info.HostConfig || {}; + const lines = []; + + if (hostConfig.RestartPolicy && hostConfig.RestartPolicy.Name && hostConfig.RestartPolicy.Name !== "no") { + let restart = hostConfig.RestartPolicy.Name; + if (restart === "on-failure" && hostConfig.RestartPolicy.MaximumRetryCount) { + restart += ":" + hostConfig.RestartPolicy.MaximumRetryCount; + } + lines.push(" --restart " + quoteSh(restart) + " \\"); + } + if (config.WorkingDir) lines.push(" -w " + quoteSh(config.WorkingDir) + " \\"); + if (config.User) lines.push(" -u " + quoteSh(config.User) + " \\"); + if (hostConfig.Privileged) lines.push(" --privileged \\"); + if (config.Tty) lines.push(" -t \\"); + if (config.OpenStdin) lines.push(" -i \\"); + + return lines; +} + +function commandTail(config) { + let entrypoint = []; + if (Array.isArray(config.Entrypoint)) { + entrypoint = config.Entrypoint.filter(item => item !== null && item !== undefined).map(String); + } else if (typeof config.Entrypoint === "string" && config.Entrypoint) { + entrypoint = [config.Entrypoint]; + } + + const cmd = Array.isArray(config.Cmd) ? config.Cmd : (typeof config.Cmd === "string" && config.Cmd ? [config.Cmd] : []); + return { + entrypoint: entrypoint.length > 0 ? entrypoint[0] : null, + args: entrypoint.slice(1).concat(cmd) + }; +} + +function buildScript(info) { + const config = info.Config || {}; + const hostConfig = info.HostConfig || {}; + const name = normalizeContainerName(info.Name); + const image = config.Image || info.Image || ""; + const tail = commandTail(config); + const lines = [ + "#!/usr/bin/env bash", + "set -euo pipefail", + "", + "name=" + quoteSh(name), + "img=" + quoteSh(image), + "", + "docker run -d \\", + " --name \"$name\" \\", + ]; + + lines.push(...collectSimpleOptions(info)); + lines.push(...collectEnv(config)); + lines.push(...collectPorts(info.NetworkSettings || {}, hostConfig)); + lines.push(...collectMounts(info)); + lines.push(...collectNetworks(info)); + + if (tail.entrypoint) { + lines.push(" --entrypoint " + quoteSh(tail.entrypoint) + " \\"); + } + + const cmd = tail.args.map(quoteSh).join(" "); + if (cmd) { + lines.push(" \"$img\" " + cmd); + } else { + lines.push(" \"$img\""); + } + + lines.push(""); + return lines.join("\n"); +} + +function outputFile(container, explicitOutput) { + if (explicitOutput) return explicitOutput; + const safe = container.replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "") || "container"; + return path.resolve(process.cwd(), "docker-run-" + safe + ".sh"); +} + +function main() { + const args = process.argv.slice(2); + if (args.length < 1 || args[0] === "-h" || args[0] === "--help") { + console.log("Usage: node restore-run.js [output-file]"); + process.exit(args.length < 1 ? 1 : 0); + } + + ensureDocker(); + const container = args[0]; + const info = inspectContainer(container); + const script = buildScript(info); + const file = outputFile(container, args[1]); + fs.writeFileSync(file, script, { mode: 0o755 }); + console.log(file); +} + +if (require.main === module) { + main(); +} + +module.exports = { + buildScript, + main +}; diff --git a/kit/src/index.js b/kit/src/index.js index bfea0f6..9d56a9e 100644 --- a/kit/src/index.js +++ b/kit/src/index.js @@ -172,6 +172,13 @@ parser.addCmdLine("freem [level]", "释放内存", function (cli) { }); +parser.addCmdLine("docker runsh [--file]", "反向获取 docker 容器启动脚本", function (cli) { + let container = cli.getParamValue("container") + let file = cli.getParamValue("file") + import("./docker/index.js").then(psl => psl.main(container, file)) +}); + + parser.addCmdLine("killport ", "释放端口", function (cli) { let port = cli.getParamValue("port") net.killport( parseInt(port, 10) ); diff --git a/kit/src/kitfunction.js b/kit/src/kitfunction.js index 0ddac34..23420d4 100644 --- a/kit/src/kitfunction.js +++ b/kit/src/kitfunction.js @@ -1,4 +1,4 @@ -const crypto = require("crypto"); +const crypto = require("crypto"); const fs = require("fs"); const os = require("os"); const path = require("path"); @@ -298,17 +298,65 @@ function writeJson(filePath, key, value) { return nextData; } +function resolvePlainPassword(params) { + if (params.plainPwd !== undefined) { + return params.plainPwd; + } + if (params.plainPassword !== undefined) { + return params.plainPassword; + } + if (params.plaintextPwd !== undefined) { + return params.plaintextPwd; + } + if (params.plaintextPassword !== undefined) { + return params.plaintextPassword; + } + return undefined; +} + +function resolveScpPassword(params) { + const plainPassword = resolvePlainPassword(params); + if (plainPassword !== undefined) { + return plainPassword; + } + return resolveValue(params.pwd || params.password); +} + function scp(from, to, params = {}) { if (!from || !to) { throw new Error("scp from and to are required"); } - platform.scp(from, to, Object.assign({}, params, { pwd: resolveValue(params.pwd) })); + const pwd = resolveScpPassword(params); + platform.scp(from, to, Object.assign({}, params, { + pwd, + password: pwd, + key: resolveValue(params.key || params.identityFile), + identityFile: resolveValue(params.identityFile || params.key) + })); } function exit(code = 0) { process.exit(code); } +function isSecretLogKey(key) { + return /^(pwd|password|plainPwd|plainPassword|plaintextPwd|plaintextPassword)$/i.test(key); +} + +function redactLogValue(value) { + if (Array.isArray(value)) { + return value.map(redactLogValue); + } + if (value && typeof value === "object") { + const result = {}; + for (const key of Object.keys(value)) { + result[key] = isSecretLogKey(key) ? "******" : redactLogValue(value[key]); + } + return result; + } + return value; +} + function stringifyLogValue(value) { if (typeof value === "string") { return value; @@ -320,7 +368,7 @@ function stringifyLogValue(value) { return `[Function ${value.name || "anonymous"}]`; } try { - return JSON.stringify(value); + return JSON.stringify(redactLogValue(value)); } catch (e) { return String(value); } diff --git a/kit/src/linux/kitfunction.js b/kit/src/linux/kitfunction.js index a61c00e..9e82d70 100644 --- a/kit/src/linux/kitfunction.js +++ b/kit/src/linux/kitfunction.js @@ -50,21 +50,68 @@ function zip(sourceDir, to, params = {}) { } } +function isRemotePath(value) { + if (typeof value !== "string") { + return false; + } + if (/^[A-Za-z]:[\\/]/.test(value)) { + return false; + } + return /^[^\s:]+:.+/.test(value); +} + +function resolveScpPath(value) { + return isRemotePath(value) ? value : path.resolve(value); +} + +function appendScpParams(args, params) { + if (params.recursive !== false) { + args.push("-r"); + } + if (params.port) { + args.push("-P", String(params.port)); + } + if (params.key) { + args.push("-i", params.key); + } + if (params.preserve) { + args.push("-p"); + } + if (params.compress) { + args.push("-C"); + } + if (params.strictHostKeyChecking !== undefined) { + args.push("-o", `StrictHostKeyChecking=${params.strictHostKeyChecking ? "yes" : "no"}`); + } + if (params.options) { + for (const name of Object.keys(params.options)) { + args.push("-o", `${name}=${params.options[name]}`); + } + } + if (params.args) { + args.push(...(Array.isArray(params.args) ? params.args : [params.args])); + } +} + function scp(from, to, params = {}) { - const source = path.resolve(from); - if (!fs.existsSync(source)) { + const source = resolveScpPath(from); + if (!isRemotePath(from) && !fs.existsSync(source)) { return; } + const args = []; + appendScpParams(args, params); + args.push(source, resolveScpPath(to)); + if (params.pwd) { if (!commandExists("sshpass")) { throw new Error("Linux scp with pwd requires sshpass in PATH"); } - run("sshpass", ["-p", params.pwd, "scp", "-r", source, to]); + run("sshpass", ["-p", params.pwd, "scp", ...args]); return; } - run("scp", ["-r", source, to]); + run("scp", args); } module.exports = { zip, scp }; diff --git a/kit/src/windows/kitfunction.js b/kit/src/windows/kitfunction.js index 87ab3b9..122f7e1 100644 --- a/kit/src/windows/kitfunction.js +++ b/kit/src/windows/kitfunction.js @@ -48,9 +48,52 @@ function zip(sourceDir, to, params = {}) { } } +function isRemotePath(value) { + if (typeof value !== "string") { + return false; + } + if (/^[A-Za-z]:[\\/]/.test(value)) { + return false; + } + return /^[^\s:]+:.+/.test(value); +} + +function resolveScpPath(value) { + return isRemotePath(value) ? value : path.resolve(value); +} + +function appendScpParams(args, params, supportOpenSshOptions) { + if (params.recursive !== false) { + args.push("-r"); + } + if (params.port) { + args.push("-P", String(params.port)); + } + if (params.key) { + args.push("-i", params.key); + } + if (params.preserve) { + args.push("-p"); + } + if (params.compress) { + args.push("-C"); + } + if (supportOpenSshOptions && params.strictHostKeyChecking !== undefined) { + args.push("-o", `StrictHostKeyChecking=${params.strictHostKeyChecking ? "yes" : "no"}`); + } + if (supportOpenSshOptions && params.options) { + for (const name of Object.keys(params.options)) { + args.push("-o", `${name}=${params.options[name]}`); + } + } + if (params.args) { + args.push(...(Array.isArray(params.args) ? params.args : [params.args])); + } +} + function scp(from, to, params = {}) { - const source = path.resolve(from); - if (!fs.existsSync(source)) { + const source = resolveScpPath(from); + if (!isRemotePath(from) && !fs.existsSync(source)) { return; } @@ -59,11 +102,20 @@ function scp(from, to, params = {}) { throw new Error("Windows scp with pwd requires pscp in PATH"); } const bin = commandExists("pscp.exe") ? "pscp.exe" : "pscp"; - run(bin, ["-pw", params.pwd, "-r", source, to]); + const args = ["-pw", params.pwd]; + appendScpParams(args, params, false); + args.push(source, resolveScpPath(to)); + run(bin, args); return; } - run("scp.exe", ["-r", source, to]); + if (!commandExists("scp.exe")) { + throw new Error("Windows scp requires scp.exe in PATH"); + } + const args = []; + appendScpParams(args, params, true); + args.push(source, resolveScpPath(to)); + run("scp.exe", args); } module.exports = { zip, scp }; diff --git a/kit/test/docker/restore-run.test.js b/kit/test/docker/restore-run.test.js new file mode 100644 index 0000000..303d536 --- /dev/null +++ b/kit/test/docker/restore-run.test.js @@ -0,0 +1,21 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); +const { buildScript } = require("./restore-run"); + +const inspectPath = path.join(__dirname, "testcase-inspect.json"); +const expectedPath = path.join(__dirname, "testcase-run.sh"); + +const inspectData = JSON.parse(fs.readFileSync(inspectPath, "utf8")); +const info = Array.isArray(inspectData) ? inspectData[0] : inspectData; +const actual = buildScript(info); +const expected = fs.readFileSync(expectedPath, "utf8").replace(/\r\n/g, "\n"); + +assert.strictEqual(actual, expected); +assert.ok(actual.includes("name=freshrss")); +assert.ok(actual.includes("img=freshrss/freshrss:latest")); +assert.ok(actual.includes('-v "$(pwd)/data:/var/www/FreshRSS/data"')); +assert.ok(actual.includes('-v "$(pwd)/extensions:/var/www/FreshRSS/extensions"')); +assert.strictEqual((actual.match(/-p 8082:80\/tcp/g) || []).length, 1); + +console.log("restore-run testcase passed"); \ No newline at end of file diff --git a/kit/test/docker/testcase-inspect.json b/kit/test/docker/testcase-inspect.json new file mode 100644 index 0000000..368f18d --- /dev/null +++ b/kit/test/docker/testcase-inspect.json @@ -0,0 +1,272 @@ +[ + { + "Id": "71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338", + "Created": "2026-06-09T08:40:16.386743045Z", + "Path": "./Docker/entrypoint.sh", + "Args": [ + "/bin/bash", + "-o", + "pipefail", + "-c", + "([ -z \"$CRON_MIN\" ] || cron) && \t. /etc/apache2/envvars && \texec apache2 -D FOREGROUND $([ -n \"$OIDC_ENABLED\" ] && [ \"$OIDC_ENABLED\" -ne 0 ] && echo \"-D OIDC_ENABLED\")" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 2178, + "ExitCode": 0, + "Error": "", + "StartedAt": "2026-06-11T03:36:17.818299057Z", + "FinishedAt": "2026-06-11T03:36:11.0220238Z" + }, + "Image": "sha256:fc6e4cf934a2d62c5da897c9fd011ef29dc3c83948ef3581d2a526fc90789622", + "ResolvConfPath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/hostname", + "HostsPath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/hosts", + "LogPath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338-json.log", + "Name": "/freshrss", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "docker-default", + "ExecIDs": null, + "HostConfig": { + "Binds": [ + "/app/freshrss/extensions:/var/www/FreshRSS/extensions", + "/app/freshrss/data:/var/www/FreshRSS/data" + ], + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": { + "max-file": "3", + "max-size": "10m" + } + }, + "NetworkMode": "bridge", + "PortBindings": { + "80/tcp": [ + { + "HostIp": "", + "HostPort": "8082" + } + ] + }, + "RestartPolicy": { + "Name": "unless-stopped", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 30, + 118 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": null, + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 0, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9-init/diff:/var/lib/docker/overlay2/e16b5110bbde48cd9070a8bea52ca1b49a6c7a6ae17abc23bc2f2152aa851dbb/diff:/var/lib/docker/overlay2/6d104177f8014ee6b291525597ac93c48cce1d6e3f45f8390330dbee8f536d89/diff:/var/lib/docker/overlay2/640758ad6ad59ac4f5825afc463879a4da93264dcbe96cfd06653d494689b1c4/diff:/var/lib/docker/overlay2/51e3461fd7fed56b0595c7c83e8c0edd6a67ae72ef9a59d2a29d6bb808847204/diff:/var/lib/docker/overlay2/b7de361f0da3dea53c4bcc25a9bbde40efaaf7933147da3b95ed8e7d4d718ad4/diff:/var/lib/docker/overlay2/1ee479d5e4ed919312b4792bec9fd9a2425ee199b4b7ff911572f272b2ba2449/diff:/var/lib/docker/overlay2/4d45f37889ffe243f232f86582a4c3b8125b13b397cacfc0193a79af39158190/diff", + "MergedDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9/merged", + "UpperDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9/diff", + "WorkDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9/work" + }, + "Name": "overlay2" + }, + "Mounts": [ + { + "Type": "bind", + "Source": "/app/freshrss/data", + "Destination": "/var/www/FreshRSS/data", + "Mode": "", + "RW": true, + "Propagation": "rprivate" + }, + { + "Type": "bind", + "Source": "/app/freshrss/extensions", + "Destination": "/var/www/FreshRSS/extensions", + "Mode": "", + "RW": true, + "Propagation": "rprivate" + } + ], + "Config": { + "Hostname": "71ba6f6111c8", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "ExposedPorts": { + "80/tcp": {} + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "TZ=Asia/Shanghai", + "CRON_MIN=*/30", + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "COPY_LOG_TO_SYSLOG=On", + "COPY_SYSLOG_TO_STDERR=On", + "DATA_PATH=", + "FRESHRSS_ENV=", + "LISTEN=", + "OIDC_ENABLED=", + "TRUSTED_PROXY=" + ], + "Cmd": [ + "/bin/bash", + "-o", + "pipefail", + "-c", + "([ -z \"$CRON_MIN\" ] || cron) && \t. /etc/apache2/envvars && \texec apache2 -D FOREGROUND $([ -n \"$OIDC_ENABLED\" ] && [ \"$OIDC_ENABLED\" -ne 0 ] && echo \"-D OIDC_ENABLED\")" + ], + "Image": "freshrss/freshrss:latest", + "Volumes": null, + "WorkingDir": "/var/www/FreshRSS", + "Entrypoint": [ + "./Docker/entrypoint.sh" + ], + "OnBuild": null, + "Labels": { + "org.opencontainers.image.created": "2026-05-20T17:58:29.110Z", + "org.opencontainers.image.description": "A free, self-hostable news aggregator…", + "org.opencontainers.image.documentation": "https://freshrss.github.io/FreshRSS/", + "org.opencontainers.image.licenses": "AGPL-3.0", + "org.opencontainers.image.revision": "b2c50115baa36c217e939ee3ea8ecfae52f91abd", + "org.opencontainers.image.source": "https://github.com/FreshRSS/FreshRSS", + "org.opencontainers.image.title": "FreshRSS", + "org.opencontainers.image.url": "https://freshrss.org/", + "org.opencontainers.image.vendor": "FreshRSS", + "org.opencontainers.image.version": "1.29.1" + } + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "78f7f8cccc0810fcdadb3d0e7bff0fe16156a46ab4b0feb7a4ebd6e80e5e330d", + "SandboxKey": "/var/run/docker/netns/78f7f8cccc08", + "Ports": { + "80/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "8082" + }, + { + "HostIp": "::", + "HostPort": "8082" + } + ] + }, + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "14c4ae2de08b7c68dfe8a757933088247f4c4ffd8c3d1236ff919da0422a655c", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.3", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "02:42:ac:11:00:03", + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "MacAddress": "02:42:ac:11:00:03", + "DriverOpts": null, + "NetworkID": "2e5dc7af2729566328476edc3fe4594e9f6b39adabbdcda5b7cc26fcbe073224", + "EndpointID": "14c4ae2de08b7c68dfe8a757933088247f4c4ffd8c3d1236ff919da0422a655c", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.3", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DNSNames": null + } + } + } + } +] \ No newline at end of file diff --git a/kit/test/docker/testcase-run.sh b/kit/test/docker/testcase-run.sh new file mode 100644 index 0000000..12c580e --- /dev/null +++ b/kit/test/docker/testcase-run.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +name=freshrss +img=freshrss/freshrss:latest + +docker run -d \ + --name "$name" \ + --restart unless-stopped \ + -w /var/www/FreshRSS \ + -e TZ=Asia/Shanghai \ + -e 'CRON_MIN=*/30' \ + -e PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + -e COPY_LOG_TO_SYSLOG=On \ + -e COPY_SYSLOG_TO_STDERR=On \ + -e DATA_PATH= \ + -e FRESHRSS_ENV= \ + -e LISTEN= \ + -e OIDC_ENABLED= \ + -e TRUSTED_PROXY= \ + -p 8082:80/tcp \ + -v "$(pwd)/data:/var/www/FreshRSS/data" \ + -v "$(pwd)/extensions:/var/www/FreshRSS/extensions" \ + --entrypoint ./Docker/entrypoint.sh \ + "$img" /bin/bash -o pipefail -c '([ -z "$CRON_MIN" ] || cron) && . /etc/apache2/envvars && exec apache2 -D FOREGROUND $([ -n "$OIDC_ENABLED" ] && [ "$OIDC_ENABLED" -ne 0 ] && echo "-D OIDC_ENABLED")' diff --git a/kit/test/remote_test.js b/kit/test/remote_test.js index ebbffc4..115c470 100644 --- a/kit/test/remote_test.js +++ b/kit/test/remote_test.js @@ -11,6 +11,32 @@ async function createRemote() { return remote } +function writeFixtureProject(tempDir, version, content) { + const projectDir = path.resolve(tempDir, `fixture-${version}`); + fs.mkdirSync(projectDir, { recursive: true }); + + const filePath = path.resolve(projectDir, "myapp.txt"); + fs.writeFileSync(filePath, content, "utf-8"); + + fs.writeFileSync(path.resolve(projectDir, "meta.json"), JSON.stringify({ + id: "myapp", + version, + desc: "test app", + official_url: "https://example.test/myapp", + dist: [ + { + path: filePath, + os: "any", + platform: "any", + filename: "myapp.txt", + type: "data" + } + ] + }, null, 4), "utf-8"); + + return projectDir; +} + let tempDir; let localStore; @@ -98,3 +124,4 @@ describe("remote module public API", () => { expect(fs.existsSync(path.resolve(localStore, "modules", "myapp", "versions", "1.0.0"))).toBe(false); }); }); +