1 /*
2 * This file is part of nzbget. See <http://nzbget.net>.
3 *
4 * Copyright (C) 2013-2016 Andrey Prygunkov <hugbug@users.sourceforge.net>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20
21 #include "nzbget.h"
22 #include "Util.h"
23 #include "FileSystem.h"
24 #include "Options.h"
25 #include "Log.h"
26 #include "ScriptConfig.h"
27
28 static const char* BEGIN_SCRIPT_SIGNATURE = "### NZBGET ";
29 static const char* POST_SCRIPT_SIGNATURE = "POST-PROCESSING";
30 static const char* SCAN_SCRIPT_SIGNATURE = "SCAN";
31 static const char* QUEUE_SCRIPT_SIGNATURE = "QUEUE";
32 static const char* SCHEDULER_SCRIPT_SIGNATURE = "SCHEDULER";
33 static const char* FEED_SCRIPT_SIGNATURE = "FEED";
34 static const char* END_SCRIPT_SIGNATURE = " SCRIPT";
35 static const char* QUEUE_EVENTS_SIGNATURE = "### QUEUE EVENTS:";
36 static const char* TASK_TIME_SIGNATURE = "### TASK TIME:";
37 static const char* DEFINITION_SIGNATURE = "###";
38
InitOptions()39 void ScriptConfig::InitOptions()
40 {
41 InitScripts();
42 InitConfigTemplates();
43 CreateTasks();
44 }
45
LoadConfig(Options::OptEntries * optEntries)46 bool ScriptConfig::LoadConfig(Options::OptEntries* optEntries)
47 {
48 // read config file
49 DiskFile infile;
50
51 if (!infile.Open(g_Options->GetConfigFilename(), DiskFile::omRead))
52 {
53 return false;
54 }
55
56 int fileLen = (int)FileSystem::FileSize(g_Options->GetConfigFilename());
57 CString buf;
58 buf.Reserve(fileLen);
59
60 while (infile.ReadLine(buf, fileLen + 1))
61 {
62 // remove trailing '\n' and '\r' and spaces
63 Util::TrimRight(buf);
64
65 // skip comments and empty lines
66 if (buf[0] == 0 || buf[0] == '#' || strspn(buf, " ") == strlen(buf))
67 {
68 continue;
69 }
70
71 CString optname;
72 CString optvalue;
73 if (Options::SplitOptionString(buf, optname, optvalue))
74 {
75 optEntries->emplace_back(optname, optvalue);
76 }
77 }
78
79 infile.Close();
80
81 Options::ConvertOldOptions(optEntries);
82
83 return true;
84 }
85
SaveConfig(Options::OptEntries * optEntries)86 bool ScriptConfig::SaveConfig(Options::OptEntries* optEntries)
87 {
88 // save to config file
89 DiskFile infile;
90
91 if (!infile.Open(g_Options->GetConfigFilename(), DiskFile::omReadWrite))
92 {
93 return false;
94 }
95
96 std::vector<CString> config;
97 std::set<Options::OptEntry*> writtenOptions;
98
99 // read config file into memory array
100 int fileLen = (int)FileSystem::FileSize(g_Options->GetConfigFilename()) + 1;
101 CString content;
102 content.Reserve(fileLen);
103 while (infile.ReadLine(content, fileLen + 1))
104 {
105 config.push_back(*content);
106 }
107 content.Clear();
108
109 // write config file back to disk, replace old values of existing options with new values
110 infile.Seek(0);
111 for (CString& buf : config)
112 {
113 const char* eq = strchr(buf, '=');
114 if (eq && buf[0] != '#')
115 {
116 // remove trailing '\n' and '\r' and spaces
117 buf.TrimRight();
118
119 CString optname;
120 CString optvalue;
121 if (g_Options->SplitOptionString(buf, optname, optvalue))
122 {
123 Options::OptEntry* optEntry = optEntries->FindOption(optname);
124 if (optEntry)
125 {
126 infile.Print("%s=%s\n", optEntry->GetName(), optEntry->GetValue());
127 writtenOptions.insert(optEntry);
128 }
129 }
130 }
131 else
132 {
133 infile.Print("%s", *buf);
134 }
135 }
136
137 // write new options
138 for (Options::OptEntry& optEntry : *optEntries)
139 {
140 std::set<Options::OptEntry*>::iterator fit = writtenOptions.find(&optEntry);
141 if (fit == writtenOptions.end())
142 {
143 infile.Print("%s=%s\n", optEntry.GetName(), optEntry.GetValue());
144 }
145 }
146
147 // close and truncate the file
148 int pos = (int)infile.Position();
149 infile.Close();
150
151 FileSystem::TruncateFile(g_Options->GetConfigFilename(), pos);
152
153 return true;
154 }
155
LoadConfigTemplates(ConfigTemplates * configTemplates)156 bool ScriptConfig::LoadConfigTemplates(ConfigTemplates* configTemplates)
157 {
158 CharBuffer buffer;
159 if (!FileSystem::LoadFileIntoBuffer(g_Options->GetConfigTemplate(), buffer, true))
160 {
161 return false;
162 }
163 configTemplates->emplace_back(Script("", ""), buffer);
164
165 if (!g_Options->GetScriptDir())
166 {
167 return true;
168 }
169
170 Scripts scriptList;
171 LoadScripts(&scriptList);
172
173 const int beginSignatureLen = strlen(BEGIN_SCRIPT_SIGNATURE);
174 const int definitionSignatureLen = strlen(DEFINITION_SIGNATURE);
175
176 for (Script& script : scriptList)
177 {
178 DiskFile infile;
179 if (!infile.Open(script.GetLocation(), DiskFile::omRead))
180 {
181 configTemplates->emplace_back(std::move(script), "");
182 continue;
183 }
184
185 StringBuilder templ;
186 char buf[1024];
187 bool inConfig = false;
188 bool inHeader = false;
189
190 while (infile.ReadLine(buf, sizeof(buf) - 1))
191 {
192 if (!strncmp(buf, BEGIN_SCRIPT_SIGNATURE, beginSignatureLen) &&
193 strstr(buf, END_SCRIPT_SIGNATURE) &&
194 (strstr(buf, POST_SCRIPT_SIGNATURE) ||
195 strstr(buf, SCAN_SCRIPT_SIGNATURE) ||
196 strstr(buf, QUEUE_SCRIPT_SIGNATURE) ||
197 strstr(buf, SCHEDULER_SCRIPT_SIGNATURE) ||
198 strstr(buf, FEED_SCRIPT_SIGNATURE)))
199 {
200 if (inConfig)
201 {
202 break;
203 }
204 inConfig = true;
205 inHeader = true;
206 continue;
207 }
208
209 inHeader &= !strncmp(buf, DEFINITION_SIGNATURE, definitionSignatureLen);
210
211 if (inConfig && !inHeader)
212 {
213 templ.Append(buf);
214 }
215 }
216
217 infile.Close();
218
219 configTemplates->emplace_back(std::move(script), templ);
220 }
221
222 return true;
223 }
224
InitConfigTemplates()225 void ScriptConfig::InitConfigTemplates()
226 {
227 if (!LoadConfigTemplates(&m_configTemplates))
228 {
229 error("Could not read configuration templates");
230 }
231 }
232
InitScripts()233 void ScriptConfig::InitScripts()
234 {
235 LoadScripts(&m_scripts);
236 }
237
LoadScripts(Scripts * scripts)238 void ScriptConfig::LoadScripts(Scripts* scripts)
239 {
240 if (Util::EmptyStr(g_Options->GetScriptDir()))
241 {
242 return;
243 }
244
245 Scripts tmpScripts;
246
247 Tokenizer tokDir(g_Options->GetScriptDir(), ",;");
248 while (const char* scriptDir = tokDir.Next())
249 {
250 LoadScriptDir(&tmpScripts, scriptDir, false);
251 }
252
253 tmpScripts.sort(
254 [](Script& script1, Script& script2)
255 {
256 return strcmp(script1.GetName(), script2.GetName()) < 0;
257 });
258
259 // first add all scripts from ScriptOrder
260 Tokenizer tokOrder(g_Options->GetScriptOrder(), ",;");
261 while (const char* scriptName = tokOrder.Next())
262 {
263 Scripts::iterator pos = std::find_if(tmpScripts.begin(), tmpScripts.end(),
264 [scriptName](Script& script)
265 {
266 return !strcmp(script.GetName(), scriptName);
267 });
268
269 if (pos != tmpScripts.end())
270 {
271 scripts->splice(scripts->end(), tmpScripts, pos);
272 }
273 }
274
275 // then add all other scripts from scripts directory
276 scripts->splice(scripts->end(), std::move(tmpScripts));
277
278 BuildScriptDisplayNames(scripts);
279 }
280
LoadScriptDir(Scripts * scripts,const char * directory,bool isSubDir)281 void ScriptConfig::LoadScriptDir(Scripts* scripts, const char* directory, bool isSubDir)
282 {
283 DirBrowser dir(directory);
284 while (const char* filename = dir.Next())
285 {
286 if (filename[0] != '.' && filename[0] != '_')
287 {
288 BString<1024> fullFilename("%s%c%s", directory, PATH_SEPARATOR, filename);
289
290 if (!FileSystem::DirectoryExists(fullFilename))
291 {
292 BString<1024> scriptName = BuildScriptName(directory, filename, isSubDir);
293 if (ScriptExists(scripts, scriptName))
294 {
295 continue;
296 }
297
298 Script script(scriptName, fullFilename);
299 if (LoadScriptFile(&script))
300 {
301 scripts->push_back(std::move(script));
302 }
303 }
304 else if (!isSubDir)
305 {
306 LoadScriptDir(scripts, fullFilename, true);
307 }
308 }
309 }
310 }
311
LoadScriptFile(Script * script)312 bool ScriptConfig::LoadScriptFile(Script* script)
313 {
314 DiskFile infile;
315 if (!infile.Open(script->GetLocation(), DiskFile::omRead))
316 {
317 return false;
318 }
319
320 CharBuffer buffer(1024 * 10 + 1);
321
322 const int beginSignatureLen = strlen(BEGIN_SCRIPT_SIGNATURE);
323 const int queueEventsSignatureLen = strlen(QUEUE_EVENTS_SIGNATURE);
324 const int taskTimeSignatureLen = strlen(TASK_TIME_SIGNATURE);
325 const int definitionSignatureLen = strlen(DEFINITION_SIGNATURE);
326
327 // check if the file contains pp-script-signature
328 // read first 10KB of the file and look for signature
329 int readBytes = (int)infile.Read(buffer, buffer.Size() - 1);
330 infile.Close();
331 buffer[readBytes] = '\0';
332
333 bool postScript = false;
334 bool scanScript = false;
335 bool queueScript = false;
336 bool schedulerScript = false;
337 bool feedScript = false;
338 char* queueEvents = nullptr;
339 char* taskTime = nullptr;
340
341 bool inConfig = false;
342 bool afterConfig = false;
343
344 // Declarations "QUEUE EVENT:" and "TASK TIME:" can be placed:
345 // - in script definition body (between opening and closing script signatures);
346 // - immediately before script definition (before opening script signature);
347 // - immediately after script definition (after closing script signature).
348 // The last two pissibilities are provided to increase compatibility of scripts with older
349 // nzbget versions which do not expect the extra declarations in the script defintion body.
350
351 Tokenizer tok(buffer, "\n\r", true);
352 while (char* line = tok.Next())
353 {
354 if (!strncmp(line, QUEUE_EVENTS_SIGNATURE, queueEventsSignatureLen))
355 {
356 queueEvents = line + queueEventsSignatureLen;
357 }
358 else if (!strncmp(line, TASK_TIME_SIGNATURE, taskTimeSignatureLen))
359 {
360 taskTime = line + taskTimeSignatureLen;
361 }
362
363 bool header = !strncmp(line, DEFINITION_SIGNATURE, definitionSignatureLen);
364 if (!header && !inConfig)
365 {
366 queueEvents = nullptr;
367 taskTime = nullptr;
368 }
369
370 if (!header && afterConfig)
371 {
372 break;
373 }
374
375 if (!strncmp(line, BEGIN_SCRIPT_SIGNATURE, beginSignatureLen) && strstr(line, END_SCRIPT_SIGNATURE))
376 {
377 if (!inConfig)
378 {
379 inConfig = true;
380 postScript = strstr(line, POST_SCRIPT_SIGNATURE);
381 scanScript = strstr(line, SCAN_SCRIPT_SIGNATURE);
382 queueScript = strstr(line, QUEUE_SCRIPT_SIGNATURE);
383 schedulerScript = strstr(line, SCHEDULER_SCRIPT_SIGNATURE);
384 feedScript = strstr(line, FEED_SCRIPT_SIGNATURE);
385 }
386 else
387 {
388 afterConfig = true;
389 }
390 }
391 }
392
393 if (!(postScript || scanScript || queueScript || schedulerScript || feedScript))
394 {
395 return false;
396 }
397
398 // trim decorations
399 char* p;
400 while (queueEvents && *queueEvents && *(p = queueEvents + strlen(queueEvents) - 1) == '#') *p = '\0';
401 if (queueEvents) queueEvents = Util::Trim(queueEvents);
402 while (taskTime && *taskTime && *(p = taskTime + strlen(taskTime) - 1) == '#') *p = '\0';
403 if (taskTime) taskTime = Util::Trim(taskTime);
404
405 script->SetPostScript(postScript);
406 script->SetScanScript(scanScript);
407 script->SetQueueScript(queueScript);
408 script->SetSchedulerScript(schedulerScript);
409 script->SetFeedScript(feedScript);
410 script->SetQueueEvents(queueEvents);
411 script->SetTaskTime(taskTime);
412
413 return true;
414 }
415
BuildScriptName(const char * directory,const char * filename,bool isSubDir)416 BString<1024> ScriptConfig::BuildScriptName(const char* directory, const char* filename, bool isSubDir)
417 {
418 if (isSubDir)
419 {
420 BString<1024> directory2 = directory;
421 int len = strlen(directory2);
422 if (directory2[len-1] == PATH_SEPARATOR || directory2[len-1] == ALT_PATH_SEPARATOR)
423 {
424 // trim last path-separator
425 directory2[len-1] = '\0';
426 }
427
428 return BString<1024>("%s%c%s", FileSystem::BaseFileName(directory2), PATH_SEPARATOR, filename);
429 }
430 else
431 {
432 return filename;
433 }
434 }
435
ScriptExists(Scripts * scripts,const char * scriptName)436 bool ScriptConfig::ScriptExists(Scripts* scripts, const char* scriptName)
437 {
438 return std::find_if(scripts->begin(), scripts->end(),
439 [scriptName](Script& script)
440 {
441 return !strcmp(script.GetName(), scriptName);
442 }) != scripts->end();
443 }
444
BuildScriptDisplayNames(Scripts * scripts)445 void ScriptConfig::BuildScriptDisplayNames(Scripts* scripts)
446 {
447 // trying to use short name without path and extension.
448 // if there are other scripts with the same short name - using a longer name instead (with ot without extension)
449
450 for (Script& script : scripts)
451 {
452 BString<1024> shortName = script.GetName();
453 if (char* ext = strrchr(shortName, '.')) *ext = '\0'; // strip file extension
454
455 const char* displayName = FileSystem::BaseFileName(shortName);
456
457 for (Script& script2 : scripts)
458 {
459 BString<1024> shortName2 = script2.GetName();
460 if (char* ext = strrchr(shortName2, '.')) *ext = '\0'; // strip file extension
461
462 const char* displayName2 = FileSystem::BaseFileName(shortName2);
463
464 if (!strcmp(displayName, displayName2) && script.GetName() != script2.GetName())
465 {
466 if (!strcmp(shortName, shortName2))
467 {
468 displayName = script.GetName();
469 }
470 else
471 {
472 displayName = shortName;
473 }
474 break;
475 }
476 }
477
478 script.SetDisplayName(displayName);
479 }
480 }
481
CreateTasks()482 void ScriptConfig::CreateTasks()
483 {
484 for (Script& script : m_scripts)
485 {
486 if (script.GetSchedulerScript() && !Util::EmptyStr(script.GetTaskTime()))
487 {
488 Tokenizer tok(g_Options->GetExtensions(), ",;");
489 while (const char* scriptName = tok.Next())
490 {
491 if (FileSystem::SameFilename(scriptName, script.GetName()))
492 {
493 g_Options->CreateSchedulerTask(0, script.GetTaskTime(),
494 nullptr, Options::scScript, script.GetName());
495 break;
496 }
497 }
498 }
499 }
500 }
501