@muhammedaksam/easiarr 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +29750 -0
- package/package.json +15 -15
- package/src/VersionInfo.ts +0 -12
- package/src/api/arr-api.ts +0 -521
- package/src/api/auto-setup-types.ts +0 -64
- package/src/api/base-api.ts +0 -216
- package/src/api/bazarr-api.ts +0 -297
- package/src/api/cloudflare-api.ts +0 -493
- package/src/api/custom-format-api.ts +0 -224
- package/src/api/grafana-api.ts +0 -230
- package/src/api/heimdall-api.ts +0 -141
- package/src/api/homarr-api.ts +0 -209
- package/src/api/huntarr-api.ts +0 -457
- package/src/api/index.ts +0 -9
- package/src/api/jellyfin-api.ts +0 -389
- package/src/api/jellyseerr-api.ts +0 -559
- package/src/api/maintainerr-api.ts +0 -461
- package/src/api/naming-config.ts +0 -101
- package/src/api/overseerr-api.ts +0 -392
- package/src/api/plex-api.ts +0 -308
- package/src/api/portainer-api.ts +0 -374
- package/src/api/profilarr-api.ts +0 -391
- package/src/api/prowlarr-api.ts +0 -488
- package/src/api/qbittorrent-api.ts +0 -300
- package/src/api/quality-profile-api.ts +0 -272
- package/src/api/tautulli-api.ts +0 -244
- package/src/api/uptime-kuma-api.ts +0 -343
- package/src/apps/categories.ts +0 -14
- package/src/apps/index.ts +0 -2
- package/src/apps/registry.ts +0 -1275
- package/src/compose/caddy-config.ts +0 -130
- package/src/compose/generator.ts +0 -287
- package/src/compose/index.ts +0 -7
- package/src/compose/templates.ts +0 -78
- package/src/compose/traefik-config.ts +0 -187
- package/src/config/bookmarks-generator.ts +0 -160
- package/src/config/defaults.ts +0 -37
- package/src/config/homepage-config.ts +0 -381
- package/src/config/index.ts +0 -6
- package/src/config/manager.ts +0 -150
- package/src/config/recyclarr-config.ts +0 -181
- package/src/config/schema.ts +0 -310
- package/src/config/slskd-config.ts +0 -86
- package/src/config/soularr-config.ts +0 -105
- package/src/config/trash-quality-definitions.ts +0 -213
- package/src/data/lidarr-custom-formats.ts +0 -127
- package/src/data/trash-profiles.ts +0 -333
- package/src/docker/client.ts +0 -312
- package/src/docker/index.ts +0 -18
- package/src/index.ts +0 -77
- package/src/setup/actions/arr-common.ts +0 -307
- package/src/setup/actions/dashboards.ts +0 -79
- package/src/setup/actions/download-clients.ts +0 -54
- package/src/setup/actions/jellyseerr.ts +0 -331
- package/src/setup/actions/media-servers.ts +0 -68
- package/src/setup/actions/monitoring.ts +0 -120
- package/src/setup/actions/prowlarr.ts +0 -137
- package/src/setup/actions/utilities.ts +0 -490
- package/src/setup/index.ts +0 -78
- package/src/setup/types.ts +0 -144
- package/src/structure/manager.ts +0 -129
- package/src/ui/App.ts +0 -111
- package/src/ui/components/ApplicationSelector.ts +0 -275
- package/src/ui/components/CredentialsForm.ts +0 -180
- package/src/ui/components/DownloadClientForm.ts +0 -114
- package/src/ui/components/FileEditor.ts +0 -91
- package/src/ui/components/FooterHint.ts +0 -119
- package/src/ui/components/PageLayout.ts +0 -160
- package/src/ui/components/UpdateNotification.ts +0 -102
- package/src/ui/index.ts +0 -7
- package/src/ui/screens/AdvancedSettings.ts +0 -188
- package/src/ui/screens/ApiKeyViewer.ts +0 -511
- package/src/ui/screens/AppConfigurator.ts +0 -543
- package/src/ui/screens/AppManager.ts +0 -284
- package/src/ui/screens/ContainerControl.ts +0 -581
- package/src/ui/screens/LogsViewer.ts +0 -478
- package/src/ui/screens/MainMenu.ts +0 -299
- package/src/ui/screens/MonitorDashboard.ts +0 -881
- package/src/ui/screens/SecretsEditor.ts +0 -216
- package/src/ui/screens/SettingsScreen.ts +0 -633
- package/src/ui/screens/index.ts +0 -22
- package/src/ui/screens/setup/BaseAppSetupScreen.ts +0 -375
- package/src/ui/screens/setup/CloudflaredSetup.ts +0 -460
- package/src/ui/screens/setup/FullAutoSetup.ts +0 -766
- package/src/ui/screens/setup/HomepageSetup.ts +0 -165
- package/src/ui/screens/setup/JellyfinSetup.ts +0 -328
- package/src/ui/screens/setup/JellyseerrSetup.ts +0 -242
- package/src/ui/screens/setup/ProwlarrSetup.ts +0 -500
- package/src/ui/screens/setup/QBittorrentSetup.ts +0 -221
- package/src/ui/screens/setup/QuickSetup.ts +0 -1169
- package/src/ui/screens/setup/RecyclarrSetup.ts +0 -270
- package/src/ui/screens/setup/TRaSHProfileSetup.ts +0 -381
- package/src/utils/arch.ts +0 -78
- package/src/utils/browser.ts +0 -27
- package/src/utils/categories.ts +0 -61
- package/src/utils/debug.ts +0 -84
- package/src/utils/env.ts +0 -138
- package/src/utils/index.ts +0 -8
- package/src/utils/logs.ts +0 -119
- package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -102
- package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -91
- package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -45
- package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -114
- package/src/utils/migrations.ts +0 -165
- package/src/utils/password.ts +0 -42
- package/src/utils/paths.ts +0 -33
- package/src/utils/unraid.ts +0 -103
- package/src/utils/update-checker.ts +0 -210
- package/src/utils/url-utils.ts +0 -38
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Caddy Configuration Generator
|
|
3
|
-
* Generates Caddyfile for automatic HTTPS reverse proxy
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { existsSync } from "node:fs"
|
|
7
|
-
import { mkdir, writeFile } from "node:fs/promises"
|
|
8
|
-
import { join } from "node:path"
|
|
9
|
-
|
|
10
|
-
import type { EasiarrConfig } from "~/config/schema"
|
|
11
|
-
import { getApp } from "~/apps/registry"
|
|
12
|
-
import { debugLog } from "~/utils/debug"
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Generate Caddyfile content for enabled apps
|
|
16
|
-
*/
|
|
17
|
-
export function generateCaddyfile(config: EasiarrConfig): string {
|
|
18
|
-
const domain = config.caddy?.domain || "localhost"
|
|
19
|
-
const email = config.caddy?.email
|
|
20
|
-
|
|
21
|
-
let caddyfile = `# Caddyfile
|
|
22
|
-
# Generated by easiarr - https://github.com/muhammedaksam/easiarr
|
|
23
|
-
#
|
|
24
|
-
# Caddy provides automatic HTTPS via Let's Encrypt.
|
|
25
|
-
# For local dev, use 'localhost' or ':80' as domain.
|
|
26
|
-
#
|
|
27
|
-
# To manually reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile
|
|
28
|
-
|
|
29
|
-
`
|
|
30
|
-
|
|
31
|
-
// Global options block (if email is set for ACME)
|
|
32
|
-
if (email) {
|
|
33
|
-
caddyfile += `{
|
|
34
|
-
email ${email}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
`
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Get enabled apps that should be proxied
|
|
41
|
-
const proxyApps = config.apps.filter((app) => {
|
|
42
|
-
if (!app.enabled) return false
|
|
43
|
-
// Skip caddy itself and apps without ports
|
|
44
|
-
if (app.id === "caddy" || app.id === "cloudflared") return false
|
|
45
|
-
const def = getApp(app.id)
|
|
46
|
-
if (!def || def.defaultPort === 0) return false
|
|
47
|
-
return true
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
// Generate reverse proxy blocks for each app
|
|
51
|
-
for (const app of proxyApps) {
|
|
52
|
-
const def = getApp(app.id)
|
|
53
|
-
if (!def) continue
|
|
54
|
-
|
|
55
|
-
const port = app.port ?? def.defaultPort
|
|
56
|
-
const subdomain = app.id
|
|
57
|
-
const internalPort = def.internalPort ?? port
|
|
58
|
-
|
|
59
|
-
// Format: subdomain.domain.com
|
|
60
|
-
const host = domain === "localhost" ? `${subdomain}.localhost` : `${subdomain}.${domain}`
|
|
61
|
-
|
|
62
|
-
caddyfile += `# ${def.name}
|
|
63
|
-
${host} {
|
|
64
|
-
reverse_proxy ${app.id}:${internalPort}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
`
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return caddyfile
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Save Caddyfile to the config directory
|
|
75
|
-
*/
|
|
76
|
-
export async function saveCaddyConfig(config: EasiarrConfig): Promise<string> {
|
|
77
|
-
// Check if Caddy is enabled
|
|
78
|
-
const caddyApp = config.apps.find((a) => a.id === "caddy" && a.enabled)
|
|
79
|
-
if (!caddyApp) {
|
|
80
|
-
debugLog("Caddy", "Caddy not enabled, skipping config generation")
|
|
81
|
-
return ""
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const caddyDir = join(config.rootDir, "config", "caddy")
|
|
85
|
-
const dataDir = join(caddyDir, "data")
|
|
86
|
-
const configDir = join(caddyDir, "config")
|
|
87
|
-
|
|
88
|
-
debugLog("Caddy", `Generating Caddyfile in ${caddyDir}`)
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
// Create directories if they don't exist
|
|
92
|
-
if (!existsSync(caddyDir)) {
|
|
93
|
-
await mkdir(caddyDir, { recursive: true })
|
|
94
|
-
}
|
|
95
|
-
if (!existsSync(dataDir)) {
|
|
96
|
-
await mkdir(dataDir, { recursive: true })
|
|
97
|
-
}
|
|
98
|
-
if (!existsSync(configDir)) {
|
|
99
|
-
await mkdir(configDir, { recursive: true })
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Generate Caddyfile
|
|
103
|
-
const caddyfileContent = generateCaddyfile(config)
|
|
104
|
-
const caddyfilePath = join(caddyDir, "Caddyfile")
|
|
105
|
-
|
|
106
|
-
// Always overwrite Caddyfile to reflect current app config
|
|
107
|
-
await writeFile(caddyfilePath, caddyfileContent, "utf-8")
|
|
108
|
-
debugLog("Caddy", `Saved Caddyfile to ${caddyfilePath}`)
|
|
109
|
-
|
|
110
|
-
return caddyfilePath
|
|
111
|
-
} catch (error) {
|
|
112
|
-
const err = error as NodeJS.ErrnoException
|
|
113
|
-
if (err.code === "EACCES") {
|
|
114
|
-
console.warn(
|
|
115
|
-
`[WARN] Cannot write Caddy config files (permission denied). ` +
|
|
116
|
-
`Fix with: sudo chown -R $(id -u):$(id -g) ${caddyDir}`
|
|
117
|
-
)
|
|
118
|
-
} else {
|
|
119
|
-
throw error
|
|
120
|
-
}
|
|
121
|
-
return ""
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Get the path to the Caddyfile
|
|
127
|
-
*/
|
|
128
|
-
export function getCaddyfilePath(config: EasiarrConfig): string {
|
|
129
|
-
return join(config.rootDir, "config", "caddy", "Caddyfile")
|
|
130
|
-
}
|
package/src/compose/generator.ts
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Docker Compose Generator
|
|
3
|
-
* Generates docker-compose.yml from easiarr configuration
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { writeFile } from "node:fs/promises"
|
|
7
|
-
|
|
8
|
-
import type { AppConfig, AppId, EasiarrConfig, TraefikConfig } from "~/config/schema"
|
|
9
|
-
import { getApp } from "~/apps/registry"
|
|
10
|
-
import { getComposePath } from "~/config/manager"
|
|
11
|
-
import { debugLog } from "~/utils/debug"
|
|
12
|
-
import { getLocalIp, updateEnv } from "~/utils/env"
|
|
13
|
-
import { saveCaddyConfig } from "./caddy-config"
|
|
14
|
-
import { generateServiceYaml } from "./templates"
|
|
15
|
-
import { saveTraefikConfig } from "./traefik-config"
|
|
16
|
-
|
|
17
|
-
export interface ComposeService {
|
|
18
|
-
image: string
|
|
19
|
-
container_name: string
|
|
20
|
-
environment: Record<string, string | number>
|
|
21
|
-
volumes: string[]
|
|
22
|
-
ports: string[]
|
|
23
|
-
restart: string
|
|
24
|
-
depends_on?: string[]
|
|
25
|
-
network_mode?: string
|
|
26
|
-
labels?: string[]
|
|
27
|
-
devices?: string[]
|
|
28
|
-
cap_add?: string[]
|
|
29
|
-
command?: string
|
|
30
|
-
/** Docker user directive (e.g., "1000:1000") for apps that don't support PUID/PGID */
|
|
31
|
-
user?: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface ComposeFile {
|
|
35
|
-
services: Record<string, ComposeService>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function generateCompose(config: EasiarrConfig): string {
|
|
39
|
-
debugLog(
|
|
40
|
-
"ComposeGenerator",
|
|
41
|
-
`Generating compose for ${config.apps.filter((a) => a.enabled).length} enabled apps`
|
|
42
|
-
)
|
|
43
|
-
const services: Record<string, ComposeService> = {}
|
|
44
|
-
|
|
45
|
-
// Track ports to move to Gluetun
|
|
46
|
-
const gluetunPorts: string[] = []
|
|
47
|
-
// Track routing decisions
|
|
48
|
-
const routedApps = new Set<string>()
|
|
49
|
-
|
|
50
|
-
// 1. Build all services first
|
|
51
|
-
for (const appConfig of config.apps) {
|
|
52
|
-
if (!appConfig.enabled) continue
|
|
53
|
-
|
|
54
|
-
const appDef = getApp(appConfig.id)
|
|
55
|
-
if (!appDef) continue
|
|
56
|
-
|
|
57
|
-
debugLog("ComposeGenerator", `Building service: ${appConfig.id}`)
|
|
58
|
-
const service = buildService(appDef, appConfig, config)
|
|
59
|
-
services[appConfig.id] = service
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 2. Apply VPN routing if enabled
|
|
63
|
-
if (config.vpn && config.vpn.mode !== "none" && services["gluetun"]) {
|
|
64
|
-
const vpnMode = config.vpn.mode
|
|
65
|
-
|
|
66
|
-
for (const [id, service] of Object.entries(services)) {
|
|
67
|
-
if (id === "gluetun") continue
|
|
68
|
-
|
|
69
|
-
const appDef = getApp(id as AppId)
|
|
70
|
-
if (!appDef) continue
|
|
71
|
-
|
|
72
|
-
// Determine if app should be routed
|
|
73
|
-
let shouldRoute = false
|
|
74
|
-
|
|
75
|
-
// Mini: Downloaders only
|
|
76
|
-
if (vpnMode === "mini" && appDef.category === "downloader") {
|
|
77
|
-
shouldRoute = true
|
|
78
|
-
}
|
|
79
|
-
// Full: Downloaders, Indexers, Requests, MediaServers, Servarr
|
|
80
|
-
else if (
|
|
81
|
-
vpnMode === "full" &&
|
|
82
|
-
["downloader", "indexer", "request", "mediaserver", "servarr"].includes(appDef.category)
|
|
83
|
-
) {
|
|
84
|
-
shouldRoute = true
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (shouldRoute) {
|
|
88
|
-
// Move ports to Gluetun
|
|
89
|
-
if (service.ports && service.ports.length > 0) {
|
|
90
|
-
gluetunPorts.push(...service.ports)
|
|
91
|
-
service.ports = []
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Set network mode
|
|
95
|
-
service.network_mode = "service:gluetun"
|
|
96
|
-
routedApps.add(id)
|
|
97
|
-
|
|
98
|
-
// Remove depends_on gluetun if it exists (circular check, though service:gluetun implies dependency)
|
|
99
|
-
// Actually docker-compose handles implied dependency for network_mode: service:xxx
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 3. Add ports to Gluetun
|
|
104
|
-
if (gluetunPorts.length > 0) {
|
|
105
|
-
services["gluetun"].ports = [
|
|
106
|
-
...new Set([...(services["gluetun"].ports || []), ...gluetunPorts]),
|
|
107
|
-
]
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return formatComposeYaml({ services })
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function buildService(
|
|
115
|
-
appDef: ReturnType<typeof getApp>,
|
|
116
|
-
appConfig: AppConfig,
|
|
117
|
-
config: EasiarrConfig
|
|
118
|
-
): ComposeService {
|
|
119
|
-
if (!appDef) throw new Error("App definition not found")
|
|
120
|
-
|
|
121
|
-
const port = appConfig.port ?? appDef.defaultPort
|
|
122
|
-
// Use ${ROOT_DIR} for volumes
|
|
123
|
-
const volumes = [...appDef.volumes("${ROOT_DIR}"), ...(appConfig.customVolumes ?? [])]
|
|
124
|
-
|
|
125
|
-
// Add log volume mount if logMount is enabled and app has logVolume defined
|
|
126
|
-
if (config.logMount && appDef.logVolume) {
|
|
127
|
-
volumes.push(`\${ROOT_DIR}/logs/${appDef.id}:${appDef.logVolume}`)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Build environment
|
|
131
|
-
const environment: Record<string, string | number> = {
|
|
132
|
-
TZ: "${TIMEZONE}",
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Add PUID/PGID (Use globals)
|
|
136
|
-
if (appDef.puid > 0 || appDef.pgid > 0 || ["jellyfin", "tautulli"].includes(appDef.id)) {
|
|
137
|
-
environment.PUID = "${PUID}"
|
|
138
|
-
environment.PGID = "${PGID}"
|
|
139
|
-
environment.UMASK = "${UMASK}"
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Add app-specific environment
|
|
143
|
-
if (appDef.environment) {
|
|
144
|
-
Object.assign(environment, appDef.environment)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Add custom environment from config
|
|
148
|
-
if (appConfig.customEnv) {
|
|
149
|
-
Object.assign(environment, appConfig.customEnv)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Build ports array
|
|
153
|
-
let ports: string[] = []
|
|
154
|
-
if (appDef.id !== "plex" && port !== 0 && appDef.defaultPort !== 0) {
|
|
155
|
-
ports.push(`"${port}:${appDef.internalPort ?? appDef.defaultPort}"`)
|
|
156
|
-
}
|
|
157
|
-
// Add secondary ports (e.g., dashboard ports)
|
|
158
|
-
if (appDef.secondaryPorts) {
|
|
159
|
-
ports = ports.concat(appDef.secondaryPorts.map((p) => `"${p}"`))
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const service: ComposeService = {
|
|
163
|
-
image: appDef.image,
|
|
164
|
-
container_name: appDef.id,
|
|
165
|
-
environment,
|
|
166
|
-
volumes,
|
|
167
|
-
ports,
|
|
168
|
-
restart: "unless-stopped",
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Add devices/caps
|
|
172
|
-
if (appDef.devices) service.devices = [...appDef.devices]
|
|
173
|
-
if (appDef.cap_add) service.cap_add = [...appDef.cap_add]
|
|
174
|
-
|
|
175
|
-
// Add command (e.g., cloudflared)
|
|
176
|
-
if (appDef.command) service.command = appDef.command
|
|
177
|
-
|
|
178
|
-
// Use Docker user directive for apps that don't support PUID/PGID (e.g., slskd)
|
|
179
|
-
if (appDef.useDockerUser) {
|
|
180
|
-
service.user = "${PUID}:${PGID}"
|
|
181
|
-
// Remove PUID/PGID env vars since they're not used
|
|
182
|
-
delete service.environment.PUID
|
|
183
|
-
delete service.environment.PGID
|
|
184
|
-
delete service.environment.UMASK
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Plex uses network_mode: host
|
|
188
|
-
if (appDef.id === "plex") {
|
|
189
|
-
service.network_mode = "host"
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Add dependencies
|
|
193
|
-
if (appDef.dependsOn && appDef.dependsOn.length > 0) {
|
|
194
|
-
const enabledDeps = appDef.dependsOn.filter((dep) =>
|
|
195
|
-
config.apps.some((a) => a.id === dep && a.enabled)
|
|
196
|
-
)
|
|
197
|
-
if (enabledDeps.length > 0) {
|
|
198
|
-
service.depends_on = enabledDeps
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
config.traefik?.enabled &&
|
|
204
|
-
appDef.id !== "plex" &&
|
|
205
|
-
appDef.id !== "cloudflared" &&
|
|
206
|
-
appDef.defaultPort !== 0
|
|
207
|
-
) {
|
|
208
|
-
if (appDef.id === "traefik") {
|
|
209
|
-
// Special labels for Traefik dashboard (accessible via traefik.domain on port 8080)
|
|
210
|
-
service.labels = generateTraefikLabels("traefik", 8080, config.traefik)
|
|
211
|
-
} else {
|
|
212
|
-
service.labels = generateTraefikLabels(
|
|
213
|
-
appDef.id,
|
|
214
|
-
appDef.internalPort ?? appDef.defaultPort,
|
|
215
|
-
config.traefik
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return service
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function generateTraefikLabels(
|
|
224
|
-
serviceName: string,
|
|
225
|
-
port: number,
|
|
226
|
-
traefik: TraefikConfig
|
|
227
|
-
): string[] {
|
|
228
|
-
const labels: string[] = [
|
|
229
|
-
"traefik.enable=true",
|
|
230
|
-
// Router
|
|
231
|
-
`traefik.http.routers.${serviceName}.service=${serviceName}`,
|
|
232
|
-
`traefik.http.routers.${serviceName}.rule=Host(\`${serviceName}.${traefik.domain}\`)`,
|
|
233
|
-
`traefik.http.routers.${serviceName}.entrypoints=${traefik.entrypoint}`,
|
|
234
|
-
]
|
|
235
|
-
|
|
236
|
-
// Add middlewares if configured
|
|
237
|
-
if (traefik.middlewares.length > 0) {
|
|
238
|
-
labels.push(`traefik.http.routers.${serviceName}.middlewares=${traefik.middlewares.join(",")}`)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Service/Load balancer
|
|
242
|
-
labels.push(
|
|
243
|
-
`traefik.http.services.${serviceName}.loadbalancer.server.scheme=http`,
|
|
244
|
-
`traefik.http.services.${serviceName}.loadbalancer.server.port=${port}`
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
return labels
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function formatComposeYaml(compose: ComposeFile): string {
|
|
251
|
-
let yaml = "---\nservices:\n"
|
|
252
|
-
|
|
253
|
-
for (const [name, service] of Object.entries(compose.services)) {
|
|
254
|
-
yaml += generateServiceYaml(name, service)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return yaml
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export async function saveCompose(config: EasiarrConfig): Promise<string> {
|
|
261
|
-
const yaml = generateCompose(config)
|
|
262
|
-
const path = getComposePath()
|
|
263
|
-
await writeFile(path, yaml, "utf-8")
|
|
264
|
-
|
|
265
|
-
// Update .env
|
|
266
|
-
await updateEnvFile(config)
|
|
267
|
-
|
|
268
|
-
// Generate Traefik config files if Traefik is enabled
|
|
269
|
-
await saveTraefikConfig(config)
|
|
270
|
-
|
|
271
|
-
// Generate Caddy config files if Caddy is enabled
|
|
272
|
-
await saveCaddyConfig(config)
|
|
273
|
-
|
|
274
|
-
return path
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async function updateEnvFile(config: EasiarrConfig) {
|
|
278
|
-
// Update .env with global configuration values
|
|
279
|
-
await updateEnv({
|
|
280
|
-
ROOT_DIR: config.rootDir,
|
|
281
|
-
TIMEZONE: config.timezone,
|
|
282
|
-
PUID: config.uid.toString(),
|
|
283
|
-
PGID: config.gid.toString(),
|
|
284
|
-
UMASK: config.umask,
|
|
285
|
-
LOCAL_DOCKER_IP: getLocalIp(),
|
|
286
|
-
})
|
|
287
|
-
}
|
package/src/compose/index.ts
DELETED
package/src/compose/templates.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Docker Compose YAML Templates
|
|
3
|
-
* Generates properly formatted YAML for services
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ComposeService } from "./generator"
|
|
7
|
-
|
|
8
|
-
export function generateServiceYaml(name: string, service: ComposeService): string {
|
|
9
|
-
let yaml = ` ${name}:\n`
|
|
10
|
-
yaml += ` image: ${service.image}\n`
|
|
11
|
-
yaml += ` container_name: ${service.container_name}\n`
|
|
12
|
-
|
|
13
|
-
// Command (for cloudflared etc.)
|
|
14
|
-
if (service.command) {
|
|
15
|
-
yaml += ` command: ${service.command}\n`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Network mode (for Plex)
|
|
19
|
-
if (service.network_mode) {
|
|
20
|
-
yaml += ` network_mode: ${service.network_mode}\n`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Dependencies
|
|
24
|
-
if (service.depends_on && service.depends_on.length > 0) {
|
|
25
|
-
yaml += ` depends_on:\n`
|
|
26
|
-
for (const dep of service.depends_on) {
|
|
27
|
-
yaml += ` - ${dep}\n`
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Environment
|
|
32
|
-
if (Object.keys(service.environment).length > 0) {
|
|
33
|
-
yaml += ` environment:\n`
|
|
34
|
-
for (const [key, value] of Object.entries(service.environment)) {
|
|
35
|
-
yaml += ` - ${key}=${value}\n`
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Volumes
|
|
40
|
-
if (service.volumes.length > 0) {
|
|
41
|
-
yaml += ` volumes:\n`
|
|
42
|
-
for (const volume of service.volumes) {
|
|
43
|
-
yaml += ` - ${volume}\n`
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Ports (skip for network_mode: host)
|
|
48
|
-
if (service.ports.length > 0 && !service.network_mode) {
|
|
49
|
-
yaml += ` ports:\n`
|
|
50
|
-
for (const port of service.ports) {
|
|
51
|
-
yaml += ` - ${port}\n`
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Labels (for Traefik etc.)
|
|
56
|
-
if (service.labels && service.labels.length > 0) {
|
|
57
|
-
yaml += ` labels:\n`
|
|
58
|
-
for (const label of service.labels) {
|
|
59
|
-
yaml += ` - ${label}\n`
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// User directive (for apps like slskd that don't support PUID/PGID)
|
|
64
|
-
if (service.user) {
|
|
65
|
-
yaml += ` user: ${service.user}\n`
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
yaml += ` restart: ${service.restart}\n\n`
|
|
69
|
-
|
|
70
|
-
return yaml
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function generateNetworkYaml(name: string, driver: string): string {
|
|
74
|
-
return `networks:
|
|
75
|
-
${name}:
|
|
76
|
-
driver: ${driver}
|
|
77
|
-
`
|
|
78
|
-
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Traefik Configuration Generator
|
|
3
|
-
* Generates traefik.yml (static config) and dynamic.yml (middlewares)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { existsSync } from "node:fs"
|
|
7
|
-
import { mkdir, writeFile } from "node:fs/promises"
|
|
8
|
-
import { join } from "node:path"
|
|
9
|
-
|
|
10
|
-
import { hashSync } from "bcrypt"
|
|
11
|
-
|
|
12
|
-
import type { EasiarrConfig } from "~/config/schema"
|
|
13
|
-
import { debugLog } from "~/utils/debug"
|
|
14
|
-
|
|
15
|
-
export interface TraefikStaticConfig {
|
|
16
|
-
entrypoints: {
|
|
17
|
-
web: { address: string }
|
|
18
|
-
}
|
|
19
|
-
api: {
|
|
20
|
-
dashboard: boolean
|
|
21
|
-
insecure: boolean
|
|
22
|
-
}
|
|
23
|
-
providers: {
|
|
24
|
-
docker: {
|
|
25
|
-
endpoint: string
|
|
26
|
-
exposedByDefault: boolean
|
|
27
|
-
}
|
|
28
|
-
file: {
|
|
29
|
-
directory: string
|
|
30
|
-
watch: boolean
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Generate traefik.yml static configuration
|
|
37
|
-
*/
|
|
38
|
-
export function generateTraefikStaticConfig(): string {
|
|
39
|
-
return `# Traefik Static Configuration
|
|
40
|
-
# Generated by easiarr
|
|
41
|
-
|
|
42
|
-
api:
|
|
43
|
-
dashboard: true
|
|
44
|
-
insecure: true
|
|
45
|
-
|
|
46
|
-
entryPoints:
|
|
47
|
-
web:
|
|
48
|
-
address: ":80"
|
|
49
|
-
|
|
50
|
-
providers:
|
|
51
|
-
docker:
|
|
52
|
-
endpoint: "unix:///var/run/docker.sock"
|
|
53
|
-
exposedByDefault: false
|
|
54
|
-
file:
|
|
55
|
-
directory: "/etc/traefik"
|
|
56
|
-
watch: true
|
|
57
|
-
`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Generate dynamic.yml with security headers middleware
|
|
62
|
-
*/
|
|
63
|
-
export function generateTraefikDynamicConfig(
|
|
64
|
-
_middlewares: string[],
|
|
65
|
-
basicAuth?: { username: string; passwordHash: string }
|
|
66
|
-
): string {
|
|
67
|
-
let yaml = `# Traefik Dynamic Configuration
|
|
68
|
-
# Generated by easiarr
|
|
69
|
-
|
|
70
|
-
http:
|
|
71
|
-
middlewares:
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
// Always include security-headers
|
|
75
|
-
yaml += ` security-headers:
|
|
76
|
-
headers:
|
|
77
|
-
browserXssFilter: true
|
|
78
|
-
contentTypeNosniff: true
|
|
79
|
-
forceSTSHeader: true
|
|
80
|
-
stsIncludeSubdomains: true
|
|
81
|
-
stsPreload: true
|
|
82
|
-
stsSeconds: 31536000
|
|
83
|
-
customFrameOptionsValue: "SAMEORIGIN"
|
|
84
|
-
`
|
|
85
|
-
|
|
86
|
-
// Add basic-auth middleware if credentials provided
|
|
87
|
-
if (basicAuth?.username && basicAuth?.passwordHash) {
|
|
88
|
-
// Escape $ in password hash for YAML
|
|
89
|
-
const escapedHash = basicAuth.passwordHash.replace(/\$/g, "$$$$")
|
|
90
|
-
yaml += `
|
|
91
|
-
basic-auth:
|
|
92
|
-
basicAuth:
|
|
93
|
-
users:
|
|
94
|
-
- "${basicAuth.username}:${escapedHash}"
|
|
95
|
-
`
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return yaml
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Generate htpasswd-compatible hash for basic auth
|
|
103
|
-
* Uses bcrypt for secure password hashing (Traefik supports bcrypt format)
|
|
104
|
-
* Format: $2b$... (bcrypt hash compatible with htpasswd)
|
|
105
|
-
*/
|
|
106
|
-
function generateHtpasswdHash(password: string): string {
|
|
107
|
-
// Use bcrypt with cost factor 10 (standard security level)
|
|
108
|
-
const hash = hashSync(password, 10)
|
|
109
|
-
// Traefik expects bcrypt hashes with $2y$ prefix (PHP-compatible)
|
|
110
|
-
// Node's bcrypt uses $2b$, which Traefik also accepts
|
|
111
|
-
return hash
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Save Traefik configuration files to the config directory
|
|
116
|
-
*/
|
|
117
|
-
export async function saveTraefikConfig(config: EasiarrConfig): Promise<void> {
|
|
118
|
-
// Check if traefik is enabled
|
|
119
|
-
const traefikApp = config.apps.find((a) => a.id === "traefik" && a.enabled)
|
|
120
|
-
if (!traefikApp) {
|
|
121
|
-
debugLog("Traefik", "Traefik not enabled, skipping config generation")
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const traefikConfigDir = join(config.rootDir, "config", "traefik")
|
|
126
|
-
const letsencryptDir = join(traefikConfigDir, "letsencrypt")
|
|
127
|
-
debugLog("Traefik", `Generating config in ${traefikConfigDir}`)
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
// Create directories if they don't exist
|
|
131
|
-
if (!existsSync(traefikConfigDir)) {
|
|
132
|
-
await mkdir(traefikConfigDir, { recursive: true })
|
|
133
|
-
debugLog("Traefik", `Created directory: ${traefikConfigDir}`)
|
|
134
|
-
}
|
|
135
|
-
if (!existsSync(letsencryptDir)) {
|
|
136
|
-
await mkdir(letsencryptDir, { recursive: true })
|
|
137
|
-
debugLog("Traefik", `Created directory: ${letsencryptDir}`)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Generate and save static config (traefik.yml)
|
|
141
|
-
const staticConfig = generateTraefikStaticConfig()
|
|
142
|
-
const staticPath = join(traefikConfigDir, "traefik.yml")
|
|
143
|
-
|
|
144
|
-
// Only write if file doesn't exist (don't overwrite user customizations)
|
|
145
|
-
if (!existsSync(staticPath)) {
|
|
146
|
-
await writeFile(staticPath, staticConfig, "utf-8")
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Check for basic auth credentials from config
|
|
150
|
-
let basicAuth: { username: string; passwordHash: string } | undefined
|
|
151
|
-
if (config.traefik?.basicAuth?.enabled) {
|
|
152
|
-
const username = config.traefik.basicAuth.username
|
|
153
|
-
const password = config.traefik.basicAuth.password
|
|
154
|
-
if (username && password) {
|
|
155
|
-
basicAuth = {
|
|
156
|
-
username,
|
|
157
|
-
passwordHash: generateHtpasswdHash(password),
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Generate and save dynamic config (dynamic.yml)
|
|
163
|
-
const dynamicConfig = generateTraefikDynamicConfig(config.traefik?.middlewares ?? [], basicAuth)
|
|
164
|
-
const dynamicPath = join(traefikConfigDir, "dynamic.yml")
|
|
165
|
-
|
|
166
|
-
// Always regenerate dynamic.yml as it contains auth settings
|
|
167
|
-
await writeFile(dynamicPath, dynamicConfig, "utf-8")
|
|
168
|
-
|
|
169
|
-
// Create acme.json with correct permissions if it doesn't exist
|
|
170
|
-
const acmePath = join(letsencryptDir, "acme.json")
|
|
171
|
-
if (!existsSync(acmePath)) {
|
|
172
|
-
await writeFile(acmePath, "{}", { mode: 0o600 })
|
|
173
|
-
}
|
|
174
|
-
} catch (error) {
|
|
175
|
-
// Permission denied - directory owned by root from Docker
|
|
176
|
-
// User needs to manually create configs or fix permissions
|
|
177
|
-
const err = error as NodeJS.ErrnoException
|
|
178
|
-
if (err.code === "EACCES") {
|
|
179
|
-
console.warn(
|
|
180
|
-
`[WARN] Cannot write Traefik config files (permission denied). ` +
|
|
181
|
-
`Fix with: sudo chown -R $(id -u):$(id -g) ${traefikConfigDir}`
|
|
182
|
-
)
|
|
183
|
-
} else {
|
|
184
|
-
throw error
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|