mirror of
https://github.com/alexta69/metube.git
synced 2026-09-21 13:35:01 +00:00
feat: allow a subscription's download folder to be changed (closes #1052)
The folder was already persisted on the subscription and already applied to every download it queued, but it was missing from the small tuple of fields the update route accepts, so it could be set when the subscription was created and never afterwards. That is the same gap the subscription name had in #1044. Add it to the accepted fields and validate it on the way in, following the validate_* helpers already in this module. The check is deliberately narrow — it rejects absolute paths and any '..' component, values that could never be valid — because the authoritative resolution stays where it already lives, in DownloadQueue at download time, along with the CUSTOM_DIRS / CREATE_CUSTOM_DIRS rules and the directory creation. Doing it this way reports a bad edit while the user is looking at the field instead of failing every check from then on, without a second copy of the path logic drifting out of step with the first. subscriptions.py cannot import ytdl.py in any case: ytdl imports _entry_id from it. An empty folder stays valid and means the base download directory. A change applies to future downloads only; files already downloaded are not moved. This covers the API side of the request. The subscriptions table does not show the folder at all today, so exposing it in the UI is a separate change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -380,6 +380,33 @@ async def test_subscriptions_update_invalid_interval_returns_error_not_500(mock_
|
||||
assert body["status"] == "error"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscriptions_update_accepts_folder(monkeypatch, mock_dqueue):
|
||||
"""Issue #1052: folder was absent from the route's accepted fields, so a
|
||||
folder-only update was rejected outright as having nothing to update."""
|
||||
submgr = MagicMock()
|
||||
submgr.update_subscription = AsyncMock(return_value={"status": "ok"})
|
||||
monkeypatch.setattr(main, "submgr", submgr)
|
||||
|
||||
req = _json_request({"id": "abc", "folder": "channels/jane"})
|
||||
resp = await main.subscriptions_update(req)
|
||||
|
||||
assert resp.status == 200
|
||||
submgr.update_subscription.assert_awaited_once_with("abc", {"folder": "channels/jane"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscriptions_update_still_drops_unknown_fields(monkeypatch, mock_dqueue):
|
||||
submgr = MagicMock()
|
||||
submgr.update_subscription = AsyncMock(return_value={"status": "ok"})
|
||||
monkeypatch.setattr(main, "submgr", submgr)
|
||||
|
||||
req = _json_request({"id": "abc", "seen_ids": ["x"], "url": "https://evil.example"})
|
||||
with pytest.raises(web.HTTPBadRequest):
|
||||
await main.subscriptions_update(req)
|
||||
submgr.update_subscription.assert_not_awaited()
|
||||
|
||||
|
||||
def test_is_within_state_dir_blocks_state_subtree():
|
||||
state_dir = main._STATE_DIR_REAL
|
||||
assert main._is_within_state_dir(state_dir)
|
||||
|
||||
@@ -891,6 +891,75 @@ class SubscriptionPersistenceTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(upd["status"], "ok")
|
||||
self.assertEqual(mgr.list_all()[0].name, "x" * 200)
|
||||
|
||||
async def test_update_subscription_changes_folder(self):
|
||||
"""Issue #1052: the folder was settable at creation and then frozen,
|
||||
because it was never added to the fields the update route accepts."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mgr = SubscriptionManager(_Config(tmp), _Queue(), _Notifier())
|
||||
sub_id = await self._add_one_subscription(mgr)
|
||||
self.assertEqual(mgr.list_all()[0].folder, "")
|
||||
|
||||
upd = await mgr.update_subscription(sub_id, {"folder": " channels/jane "})
|
||||
self.assertEqual(upd["status"], "ok")
|
||||
self.assertEqual(upd["subscription"]["folder"], "channels/jane")
|
||||
self.assertEqual(mgr.list_all()[0].folder, "channels/jane")
|
||||
|
||||
async def test_update_subscription_folder_survives_reload(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cfg = _Config(tmp)
|
||||
mgr = SubscriptionManager(cfg, _Queue(), _Notifier())
|
||||
sub_id = await self._add_one_subscription(mgr)
|
||||
await mgr.update_subscription(sub_id, {"folder": "archive"})
|
||||
|
||||
reloaded = SubscriptionManager(cfg, _Queue(), _Notifier())
|
||||
self.assertEqual(reloaded.get(sub_id).folder, "archive")
|
||||
|
||||
async def test_update_subscription_clears_folder(self):
|
||||
# An empty folder is valid and means the base download directory.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mgr = SubscriptionManager(_Config(tmp), _Queue(), _Notifier())
|
||||
sub_id = await self._add_one_subscription(mgr)
|
||||
await mgr.update_subscription(sub_id, {"folder": "archive"})
|
||||
|
||||
upd = await mgr.update_subscription(sub_id, {"folder": " "})
|
||||
self.assertEqual(upd["status"], "ok")
|
||||
self.assertEqual(mgr.list_all()[0].folder, "")
|
||||
|
||||
async def test_update_subscription_rejects_unusable_folder(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mgr = SubscriptionManager(_Config(tmp), _Queue(), _Notifier())
|
||||
sub_id = await self._add_one_subscription(mgr)
|
||||
await mgr.update_subscription(sub_id, {"folder": "keep"})
|
||||
|
||||
bad_values = (
|
||||
"/etc",
|
||||
"/absolute/path",
|
||||
"../escape",
|
||||
"nested/../../escape",
|
||||
"windows\\..\\escape",
|
||||
42,
|
||||
["a"],
|
||||
)
|
||||
for bad in bad_values:
|
||||
upd = await mgr.update_subscription(sub_id, {"folder": bad})
|
||||
self.assertEqual(upd["status"], "error", f"expected {bad!r} to be rejected")
|
||||
self.assertEqual(mgr.list_all()[0].folder, "keep")
|
||||
|
||||
async def test_update_subscription_folder_leaves_other_fields_alone(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mgr = SubscriptionManager(_Config(tmp), _Queue(), _Notifier())
|
||||
sub_id = await self._add_one_subscription(mgr)
|
||||
before = mgr.get(sub_id)
|
||||
name, interval, enabled = before.name, before.check_interval_minutes, before.enabled
|
||||
|
||||
await mgr.update_subscription(sub_id, {"folder": "only/this"})
|
||||
|
||||
after = mgr.get(sub_id)
|
||||
self.assertEqual(after.folder, "only/this")
|
||||
self.assertEqual(after.name, name)
|
||||
self.assertEqual(after.check_interval_minutes, interval)
|
||||
self.assertEqual(after.enabled, enabled)
|
||||
|
||||
async def test_update_subscription_skip_subscriber_only(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
queue = _Queue()
|
||||
|
||||
Reference in New Issue
Block a user