Repository: sissbruecker/linkding (python)

Issue:
Promote single-file timeout to env variable
https://github.com/sissbruecker/linkding/blob/5d8fdebb7c4918c3face9478543ccf86b2db47ef/bookmarks/services/singlefile.py#L27

Can you please promote single-file timeout value to an environment variable? I run linkding on a raspberry pi and sometimes taking a snapshot is sloooow. 

For example:
```
time single-file https://www.hightidestoredtla.shop/products/bullet-ballpoint-pen-light?variant=38
087018184895

real    1m31.228s
user    0m13.667s
sys     0m11.835s
```

In my case I'd like to set a maybe 2 or 3 minute timeout. 

Pull request:
Add option for customizing single-file timeout
Closes #685 

Testing procedure (I did not build & deploy a container):

1. Recreate container with `LD_SINGLEFILE_TIMEOUT_SEC=360` in the compose file. 
2. `docker exec` into the running container, modify `base.py` and `singlefile.py` per below 
3. `docker restart` the container. 
4. Verified that the URL mentioned in above issue worked with a 360 second timeout on Raspberry Pi 4. 

Patch:
diff --git a/bookmarks/services/singlefile.py b/bookmarks/services/singlefile.py
index 7ccc3a8..755cffc 100644
--- a/bookmarks/services/singlefile.py
+++ b/bookmarks/services/singlefile.py
@@ -24,7 +24,7 @@ def create_snapshot(url: str, filepath: str):
     try:
         # Use start_new_session=True to create a new process group
         process = subprocess.Popen(args, start_new_session=True)
-        process.wait(timeout=60)
+        process.wait(timeout=settings.LD_SINGLEFILE_TIMEOUT_SEC)
 
         # check if the file was created
         if not os.path.exists(temp_filepath):
diff --git a/docs/Options.md b/docs/Options.md
index 9d2bb63..e5c9fae 100644
--- a/docs/Options.md
+++ b/docs/Options.md
@@ -246,3 +246,10 @@ See the default URL for how to insert the placeholder to the favicon provider UR
 
 Alternative favicon providers:
 - DuckDuckGo: `https://icons.duckduckgo.com/ip3/{domain}.ico`
+
+
+### `LD_SINGLEFILE_TIMEOUT_SEC`
+
+Values: `Float` | Default =  60.0
+
+When creating archive snapshots, control the timeout for how long to wait for `single-file` to complete, in `seconds`. Defaults to 60 seconds; on lower-powered hardware you may need to increase this value. 
\ No newline at end of file
diff --git a/siteroot/settings/base.py b/siteroot/settings/base.py
index b0116a6..619425b 100644
--- a/siteroot/settings/base.py
+++ b/siteroot/settings/base.py
@@ -295,6 +295,7 @@ LD_ENABLE_SNAPSHOTS = os.getenv("LD_ENABLE_SNAPSHOTS", False) in (
 )
 LD_SINGLEFILE_PATH = os.getenv("LD_SINGLEFILE_PATH", "single-file")
 LD_SINGLEFILE_OPTIONS = os.getenv("LD_SINGLEFILE_OPTIONS", "")
+LD_SINGLEFILE_TIMEOUT_SEC = float(os.getenv("LD_SINGLEFILE_TIMEOUT_SEC", 60))
 
 # Monolith isn't used at the moment, as the local snapshot implementation
 # switched to single-file after the prototype. Keeping this around in case


Test patch:
diff --git a/bookmarks/tests/test_singlefile_service.py b/bookmarks/tests/test_singlefile_service.py
index c592f06..f433776 100644
--- a/bookmarks/tests/test_singlefile_service.py
+++ b/bookmarks/tests/test_singlefile_service.py
@@ -3,7 +3,7 @@ import os
 import subprocess
 from unittest import mock
 
-from django.test import TestCase
+from django.test import TestCase, override_settings
 
 from bookmarks.services import singlefile
 
@@ -50,3 +50,24 @@ class SingleFileServiceTestCase(TestCase):
         with mock.patch("subprocess.Popen"):
             with self.assertRaises(singlefile.SingeFileError):
                 singlefile.create_snapshot("http://example.com", self.html_filepath)
+
+    def test_create_snapshot_default_timeout_setting(self):
+        mock_process = mock.Mock()
+        mock_process.wait.return_value = 0
+        self.create_test_file()
+
+        with mock.patch("subprocess.Popen", return_value=mock_process):
+            singlefile.create_snapshot("http://example.com", self.html_filepath)
+
+            mock_process.wait.assert_called_with(timeout=60)
+
+    @override_settings(LD_SINGLEFILE_TIMEOUT_SEC=120)
+    def test_create_snapshot_custom_timeout_setting(self):
+        mock_process = mock.Mock()
+        mock_process.wait.return_value = 0
+        self.create_test_file()
+
+        with mock.patch("subprocess.Popen", return_value=mock_process):
+            singlefile.create_snapshot("http://example.com", self.html_filepath)
+
+            mock_process.wait.assert_called_with(timeout=120)