@muhammedaksam/easiarr 0.1.10 → 0.3.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/package.json +1 -1
- package/src/api/arr-api.ts +51 -36
- package/src/api/custom-format-api.ts +212 -0
- package/src/api/prowlarr-api.ts +301 -0
- package/src/api/qbittorrent-api.ts +198 -0
- package/src/api/quality-profile-api.ts +205 -0
- package/src/apps/registry.ts +6 -6
- package/src/compose/generator.ts +10 -28
- package/src/data/trash-profiles.ts +252 -0
- package/src/ui/screens/AdvancedSettings.ts +2 -1
- package/src/ui/screens/ApiKeyViewer.ts +5 -23
- package/src/ui/screens/AppConfigurator.ts +53 -93
- package/src/ui/screens/MainMenu.ts +33 -1
- package/src/ui/screens/ProwlarrSetup.ts +376 -0
- package/src/ui/screens/SecretsEditor.ts +9 -50
- package/src/ui/screens/TRaSHProfileSetup.ts +371 -0
- package/src/utils/categories.ts +59 -0
- package/src/utils/debug.ts +27 -0
- package/src/utils/env.ts +98 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qBittorrent WebAPI Client
|
|
3
|
+
* Configures qBittorrent settings via API
|
|
4
|
+
* API docs: https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface QBittorrentPreferences {
|
|
8
|
+
save_path?: string
|
|
9
|
+
temp_path_enabled?: boolean
|
|
10
|
+
temp_path?: string
|
|
11
|
+
auto_tmm_enabled?: boolean
|
|
12
|
+
category_changed_tmm_enabled?: boolean
|
|
13
|
+
save_path_changed_tmm_enabled?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface QBittorrentCategory {
|
|
17
|
+
name: string
|
|
18
|
+
savePath: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class QBittorrentClient {
|
|
22
|
+
private baseUrl: string
|
|
23
|
+
private username: string
|
|
24
|
+
private password: string
|
|
25
|
+
private cookie: string | null = null
|
|
26
|
+
|
|
27
|
+
constructor(host: string, port: number, username: string, password: string) {
|
|
28
|
+
this.baseUrl = `http://${host}:${port}`
|
|
29
|
+
this.username = username
|
|
30
|
+
this.password = password
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Authenticate with qBittorrent WebUI
|
|
35
|
+
* POST /api/v2/auth/login
|
|
36
|
+
*/
|
|
37
|
+
async login(): Promise<boolean> {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch(`${this.baseUrl}/api/v2/auth/login`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
42
|
+
body: `username=${encodeURIComponent(this.username)}&password=${encodeURIComponent(this.password)}`,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (!response.ok) return false
|
|
46
|
+
|
|
47
|
+
// Extract SID cookie from response
|
|
48
|
+
const setCookie = response.headers.get("set-cookie")
|
|
49
|
+
if (setCookie) {
|
|
50
|
+
const match = setCookie.match(/SID=([^;]+)/)
|
|
51
|
+
if (match) {
|
|
52
|
+
this.cookie = `SID=${match[1]}`
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check response text for "Ok."
|
|
58
|
+
const text = await response.text()
|
|
59
|
+
return text === "Ok."
|
|
60
|
+
} catch {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if connected to qBittorrent
|
|
67
|
+
*/
|
|
68
|
+
async isConnected(): Promise<boolean> {
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(`${this.baseUrl}/api/v2/app/version`, {
|
|
71
|
+
headers: this.cookie ? { Cookie: this.cookie } : {},
|
|
72
|
+
})
|
|
73
|
+
return response.ok
|
|
74
|
+
} catch {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get current preferences
|
|
81
|
+
* GET /api/v2/app/preferences
|
|
82
|
+
*/
|
|
83
|
+
async getPreferences(): Promise<QBittorrentPreferences> {
|
|
84
|
+
const response = await fetch(`${this.baseUrl}/api/v2/app/preferences`, {
|
|
85
|
+
headers: this.cookie ? { Cookie: this.cookie } : {},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(`Failed to get preferences: ${response.status}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return response.json()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Set preferences
|
|
97
|
+
* POST /api/v2/app/setPreferences
|
|
98
|
+
*/
|
|
99
|
+
async setPreferences(prefs: QBittorrentPreferences): Promise<void> {
|
|
100
|
+
const response = await fetch(`${this.baseUrl}/api/v2/app/setPreferences`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
104
|
+
...(this.cookie ? { Cookie: this.cookie } : {}),
|
|
105
|
+
},
|
|
106
|
+
body: `json=${encodeURIComponent(JSON.stringify(prefs))}`,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw new Error(`Failed to set preferences: ${response.status}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get all categories
|
|
116
|
+
* GET /api/v2/torrents/categories
|
|
117
|
+
*/
|
|
118
|
+
async getCategories(): Promise<Record<string, { name: string; savePath: string }>> {
|
|
119
|
+
const response = await fetch(`${this.baseUrl}/api/v2/torrents/categories`, {
|
|
120
|
+
headers: this.cookie ? { Cookie: this.cookie } : {},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`Failed to get categories: ${response.status}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return response.json()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a category
|
|
132
|
+
* POST /api/v2/torrents/createCategory
|
|
133
|
+
*/
|
|
134
|
+
async createCategory(name: string, savePath: string): Promise<void> {
|
|
135
|
+
const response = await fetch(`${this.baseUrl}/api/v2/torrents/createCategory`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: {
|
|
138
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
139
|
+
...(this.cookie ? { Cookie: this.cookie } : {}),
|
|
140
|
+
},
|
|
141
|
+
body: `category=${encodeURIComponent(name)}&savePath=${encodeURIComponent(savePath)}`,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// 409 means category already exists - that's OK
|
|
145
|
+
if (!response.ok && response.status !== 409) {
|
|
146
|
+
throw new Error(`Failed to create category: ${response.status}`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Edit a category's save path
|
|
152
|
+
* POST /api/v2/torrents/editCategory
|
|
153
|
+
*/
|
|
154
|
+
async editCategory(name: string, savePath: string): Promise<void> {
|
|
155
|
+
const response = await fetch(`${this.baseUrl}/api/v2/torrents/editCategory`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
159
|
+
...(this.cookie ? { Cookie: this.cookie } : {}),
|
|
160
|
+
},
|
|
161
|
+
body: `category=${encodeURIComponent(name)}&savePath=${encodeURIComponent(savePath)}`,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
throw new Error(`Failed to edit category: ${response.status}`)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Configure qBittorrent for TRaSH Guide compliance
|
|
171
|
+
* Sets proper save paths and creates categories based on enabled apps
|
|
172
|
+
* @param categories - Array of {name, savePath} for each enabled *arr app
|
|
173
|
+
*/
|
|
174
|
+
async configureTRaSHCompliant(categories: QBittorrentCategory[] = []): Promise<void> {
|
|
175
|
+
// 1. Set global preferences
|
|
176
|
+
await this.setPreferences({
|
|
177
|
+
save_path: "/data/torrents",
|
|
178
|
+
temp_path_enabled: false,
|
|
179
|
+
auto_tmm_enabled: true,
|
|
180
|
+
category_changed_tmm_enabled: true,
|
|
181
|
+
save_path_changed_tmm_enabled: true,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// 2. Create categories for each enabled media type
|
|
185
|
+
for (const cat of categories) {
|
|
186
|
+
try {
|
|
187
|
+
await this.createCategory(cat.name, cat.savePath)
|
|
188
|
+
} catch {
|
|
189
|
+
// Try to update existing category
|
|
190
|
+
try {
|
|
191
|
+
await this.editCategory(cat.name, cat.savePath)
|
|
192
|
+
} catch {
|
|
193
|
+
// Ignore - category might not exist or be locked
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Profile API Client
|
|
3
|
+
* Manages Quality Profiles and Custom Format scoring for Radarr/Sonarr
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface QualityItem {
|
|
7
|
+
id?: number
|
|
8
|
+
name?: string
|
|
9
|
+
quality?: {
|
|
10
|
+
id: number
|
|
11
|
+
name: string
|
|
12
|
+
source: string
|
|
13
|
+
resolution: number
|
|
14
|
+
}
|
|
15
|
+
items?: QualityItem[]
|
|
16
|
+
allowed: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FormatItem {
|
|
20
|
+
format: number
|
|
21
|
+
name?: string
|
|
22
|
+
score: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface QualityProfile {
|
|
26
|
+
id?: number
|
|
27
|
+
name: string
|
|
28
|
+
upgradeAllowed: boolean
|
|
29
|
+
cutoff: number
|
|
30
|
+
minFormatScore: number
|
|
31
|
+
cutoffFormatScore: number
|
|
32
|
+
formatItems: FormatItem[]
|
|
33
|
+
language?: { id: number; name: string }
|
|
34
|
+
items: QualityItem[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface QualityDefinition {
|
|
38
|
+
id: number
|
|
39
|
+
quality: {
|
|
40
|
+
id: number
|
|
41
|
+
name: string
|
|
42
|
+
source: string
|
|
43
|
+
resolution: number
|
|
44
|
+
}
|
|
45
|
+
title: string
|
|
46
|
+
weight: number
|
|
47
|
+
minSize: number
|
|
48
|
+
maxSize: number
|
|
49
|
+
preferredSize: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class QualityProfileClient {
|
|
53
|
+
private baseUrl: string
|
|
54
|
+
private apiKey: string
|
|
55
|
+
private apiVersion: string
|
|
56
|
+
|
|
57
|
+
constructor(host: string, port: number, apiKey: string, apiVersion = "v3") {
|
|
58
|
+
this.baseUrl = `http://${host}:${port}/api/${apiVersion}`
|
|
59
|
+
this.apiKey = apiKey
|
|
60
|
+
this.apiVersion = apiVersion
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
64
|
+
const url = `${this.baseUrl}${endpoint}`
|
|
65
|
+
const headers: Record<string, string> = {
|
|
66
|
+
"X-Api-Key": this.apiKey,
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
...((options.headers as Record<string, string>) || {}),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const response = await fetch(url, { ...options, headers })
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const text = await response.text()
|
|
78
|
+
if (!text) return {} as T
|
|
79
|
+
return JSON.parse(text) as T
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Quality Profile methods
|
|
83
|
+
async getQualityProfiles(): Promise<QualityProfile[]> {
|
|
84
|
+
return this.request<QualityProfile[]>("/qualityprofile")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getQualityProfile(id: number): Promise<QualityProfile> {
|
|
88
|
+
return this.request<QualityProfile>(`/qualityprofile/${id}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async createQualityProfile(profile: Omit<QualityProfile, "id">): Promise<QualityProfile> {
|
|
92
|
+
return this.request<QualityProfile>("/qualityprofile", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body: JSON.stringify(profile),
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async updateQualityProfile(id: number, profile: QualityProfile): Promise<QualityProfile> {
|
|
99
|
+
return this.request<QualityProfile>(`/qualityprofile/${id}`, {
|
|
100
|
+
method: "PUT",
|
|
101
|
+
body: JSON.stringify({ ...profile, id }),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async deleteQualityProfile(id: number): Promise<void> {
|
|
106
|
+
await this.request(`/qualityprofile/${id}`, { method: "DELETE" })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Quality Definition methods (for size limits)
|
|
110
|
+
async getQualityDefinitions(): Promise<QualityDefinition[]> {
|
|
111
|
+
return this.request<QualityDefinition[]>("/qualitydefinition")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async updateQualityDefinitions(definitions: QualityDefinition[]): Promise<QualityDefinition[]> {
|
|
115
|
+
return this.request<QualityDefinition[]>("/qualitydefinition/update", {
|
|
116
|
+
method: "PUT",
|
|
117
|
+
body: JSON.stringify(definitions),
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Helper: Get quality by name from existing profiles
|
|
122
|
+
async getQualityIdByName(name: string): Promise<number | null> {
|
|
123
|
+
const profiles = await this.getQualityProfiles()
|
|
124
|
+
if (profiles.length === 0) return null
|
|
125
|
+
|
|
126
|
+
const findQuality = (items: QualityItem[]): number | null => {
|
|
127
|
+
for (const item of items) {
|
|
128
|
+
if (item.quality?.name === name) return item.quality.id
|
|
129
|
+
if (item.items) {
|
|
130
|
+
const found = findQuality(item.items)
|
|
131
|
+
if (found !== null) return found
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return findQuality(profiles[0].items)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create TRaSH-recommended profile
|
|
141
|
+
async createTRaSHProfile(
|
|
142
|
+
name: string,
|
|
143
|
+
cutoffQualityName: string,
|
|
144
|
+
allowedQualities: string[],
|
|
145
|
+
cfScores: Record<string, number> = {}
|
|
146
|
+
): Promise<QualityProfile> {
|
|
147
|
+
// Get existing profile to clone quality structure
|
|
148
|
+
const existingProfiles = await this.getQualityProfiles()
|
|
149
|
+
if (existingProfiles.length === 0) {
|
|
150
|
+
throw new Error("No existing profiles to clone quality structure from")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const baseProfile = existingProfiles[0]
|
|
154
|
+
const cutoffId = await this.getQualityIdByName(cutoffQualityName)
|
|
155
|
+
|
|
156
|
+
if (cutoffId === null) {
|
|
157
|
+
throw new Error(`Quality "${cutoffQualityName}" not found`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Build quality items with allowed flags
|
|
161
|
+
const setAllowed = (items: QualityItem[]): QualityItem[] => {
|
|
162
|
+
return items.map((item) => {
|
|
163
|
+
if (item.items) {
|
|
164
|
+
return { ...item, items: setAllowed(item.items) }
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
...item,
|
|
168
|
+
allowed: item.quality ? allowedQualities.includes(item.quality.name) : false,
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Get custom formats and map scores
|
|
174
|
+
const formatItems: FormatItem[] = baseProfile.formatItems.map((fi) => ({
|
|
175
|
+
format: fi.format,
|
|
176
|
+
name: fi.name,
|
|
177
|
+
score: cfScores[fi.name || ""] ?? 0,
|
|
178
|
+
}))
|
|
179
|
+
|
|
180
|
+
const newProfile: Omit<QualityProfile, "id"> = {
|
|
181
|
+
name,
|
|
182
|
+
upgradeAllowed: true,
|
|
183
|
+
cutoff: cutoffId,
|
|
184
|
+
minFormatScore: 0,
|
|
185
|
+
cutoffFormatScore: 10000,
|
|
186
|
+
formatItems,
|
|
187
|
+
language: baseProfile.language,
|
|
188
|
+
items: setAllowed(baseProfile.items),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return this.createQualityProfile(newProfile)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Update CF scores on existing profile
|
|
195
|
+
async updateProfileCFScores(profileId: number, cfScores: Record<string, number>): Promise<QualityProfile> {
|
|
196
|
+
const profile = await this.getQualityProfile(profileId)
|
|
197
|
+
|
|
198
|
+
profile.formatItems = profile.formatItems.map((fi) => ({
|
|
199
|
+
...fi,
|
|
200
|
+
score: cfScores[fi.name || ""] ?? fi.score,
|
|
201
|
+
}))
|
|
202
|
+
|
|
203
|
+
return this.updateQualityProfile(profileId, profile)
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/apps/registry.ts
CHANGED
|
@@ -138,10 +138,8 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
138
138
|
enabledKey: "api_enabled",
|
|
139
139
|
generateIfMissing: true,
|
|
140
140
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
apiVersion: "v1",
|
|
144
|
-
},
|
|
141
|
+
// Note: Mylar3 is NOT an *arr app - has different API format (?cmd=<endpoint>)
|
|
142
|
+
// Root folder is configured via Web UI settings, not API
|
|
145
143
|
},
|
|
146
144
|
|
|
147
145
|
whisparr: {
|
|
@@ -246,7 +244,8 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
246
244
|
image: "lscr.io/linuxserver/qbittorrent:latest",
|
|
247
245
|
puid: 13007,
|
|
248
246
|
pgid: 13000,
|
|
249
|
-
|
|
247
|
+
// TRaSH: Mount full /data for consistent paths with *arr apps (enables hardlinks)
|
|
248
|
+
volumes: (root) => [`${root}/config/qbittorrent:/config`, `${root}/data:/data`],
|
|
250
249
|
environment: { WEBUI_PORT: "8080" },
|
|
251
250
|
trashGuide: "docs/Downloaders/qBittorrent/",
|
|
252
251
|
},
|
|
@@ -260,7 +259,8 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
260
259
|
image: "lscr.io/linuxserver/sabnzbd:latest",
|
|
261
260
|
puid: 13011,
|
|
262
261
|
pgid: 13000,
|
|
263
|
-
|
|
262
|
+
// TRaSH: Mount full /data for consistent paths with *arr apps (enables hardlinks)
|
|
263
|
+
volumes: (root) => [`${root}/config/sabnzbd:/config`, `${root}/data:/data`],
|
|
264
264
|
|
|
265
265
|
trashGuide: "docs/Downloaders/SABnzbd/",
|
|
266
266
|
apiKeyMeta: {
|
package/src/compose/generator.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Generates docker-compose.yml from Easiarr configuration
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { writeFile
|
|
7
|
-
import { existsSync } from "node:fs"
|
|
6
|
+
import { writeFile } from "node:fs/promises"
|
|
8
7
|
import type { EasiarrConfig, AppConfig, TraefikConfig, AppId } from "../config/schema"
|
|
9
8
|
import { getComposePath } from "../config/manager"
|
|
10
9
|
import { getApp } from "../apps/registry"
|
|
11
10
|
import { generateServiceYaml } from "./templates"
|
|
11
|
+
import { updateEnv } from "../utils/env"
|
|
12
12
|
|
|
13
13
|
export interface ComposeService {
|
|
14
14
|
image: string
|
|
@@ -205,30 +205,12 @@ export async function saveCompose(config: EasiarrConfig): Promise<string> {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
async function updateEnvFile(config: EasiarrConfig) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const [key, ...val] = line.split("=")
|
|
217
|
-
if (key && val) currentEnv[key.trim()] = val.join("=").trim()
|
|
218
|
-
})
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Update/Set globals
|
|
222
|
-
currentEnv["ROOT_DIR"] = config.rootDir
|
|
223
|
-
currentEnv["TIMEZONE"] = config.timezone
|
|
224
|
-
currentEnv["PUID"] = config.uid.toString()
|
|
225
|
-
currentEnv["PGID"] = config.gid.toString()
|
|
226
|
-
currentEnv["UMASK"] = config.umask
|
|
227
|
-
|
|
228
|
-
// Reconstruct .env content
|
|
229
|
-
envContent = Object.entries(currentEnv)
|
|
230
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
231
|
-
.join("\n")
|
|
232
|
-
|
|
233
|
-
await writeFile(envPath, envContent, "utf-8")
|
|
208
|
+
// Update .env with global configuration values
|
|
209
|
+
await updateEnv({
|
|
210
|
+
ROOT_DIR: config.rootDir,
|
|
211
|
+
TIMEZONE: config.timezone,
|
|
212
|
+
PUID: config.uid.toString(),
|
|
213
|
+
PGID: config.gid.toString(),
|
|
214
|
+
UMASK: config.umask,
|
|
215
|
+
})
|
|
234
216
|
}
|