mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
WROL Mode: refuse video edits and transcodes past the API
The routes already returned 403, but a transcode Job queued before WROL Mode was enabled still ran, update_video could be called from anywhere, and the Edit modal's Save button stayed live. The Job now fails before ffmpeg starts, update_video carries wrol_mode_check, and the modal disables Save with a notice. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
749d5ac32b
commit
d5917749a7
6 changed files with 62 additions and 2 deletions
|
|
@ -285,6 +285,7 @@ export function TranscodeModal({open, onClose, fileGroupId, video, onComplete})
|
|||
* refresh; a re-download of the Video's metadata will overwrite it.
|
||||
*/
|
||||
export function EditVideoModal({open, onClose, fileGroupId, videoFile, description: currentDescription, onSaved}) {
|
||||
const wrolModeEnabled = useWROLMode();
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -345,6 +346,9 @@ export function EditVideoModal({open, onClose, fileGroupId, videoFile, descripti
|
|||
<Text size='sm' c='var(--muted)' mt='sm'>
|
||||
Saved to the video's info json. Refreshing the video from its source will replace it.
|
||||
</Text>
|
||||
{wrolModeEnabled && <Text size='sm' c='var(--danger)' mt='sm'>
|
||||
Videos cannot be edited while WROL Mode is enabled.
|
||||
</Text>}
|
||||
</form>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
|
|
@ -354,7 +358,7 @@ export function EditVideoModal({open, onClose, fileGroupId, videoFile, descripti
|
|||
icon='save'
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={saving || !trimmed || unchanged}
|
||||
disabled={saving || !trimmed || unchanged || !!wrolModeEnabled}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -160,6 +160,16 @@ describe('EditVideoModal', () => {
|
|||
await waitFor(() => expect(updateVideo).toHaveBeenCalledWith(7, {description: ''}));
|
||||
});
|
||||
|
||||
test('WROL Mode disables saving', async () => {
|
||||
renderWithProviders(
|
||||
<EditVideoModal open={true} onClose={jest.fn()} fileGroupId={7} videoFile={videoFile}/>,
|
||||
wrolMode,
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('Title'), {target: {value: 'Changed'}});
|
||||
expect(screen.getByRole('button', {name: /Save/})).toBeDisabled();
|
||||
expect(screen.getByText(/WROL Mode/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('an empty title cannot be saved', () => {
|
||||
renderWithProviders(<EditVideoModal open={true} onClose={jest.fn()} fileGroupId={7} videoFile={videoFile}/>);
|
||||
fireEvent.change(screen.getByLabelText('Title'), {target: {value: ' '}});
|
||||
|
|
|
|||
|
|
@ -164,3 +164,26 @@ async def test_video_transcode_api(async_client, test_session, video_factory, wr
|
|||
content=json.dumps({'video_codec': 'h264'}))
|
||||
assert response.status == 403
|
||||
await wrol_mode_fixture(False)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transcode_video_job_wrol_mode(test_session, test_directory, async_client, video_factory,
|
||||
wrol_mode_fixture):
|
||||
"""A transcode queued before WROL Mode was enabled must not run: the Job fails, ffmpeg is never
|
||||
started, and the file is untouched."""
|
||||
video = video_factory()
|
||||
video_path = video.video_path
|
||||
before = video_path.read_bytes()
|
||||
|
||||
job_id = transcode_video_job.enqueue(file_group_id=video.file_group_id, video_codec='h264')
|
||||
await wrol_mode_fixture(True)
|
||||
try:
|
||||
with mock.patch('wrolpi.cmd.run_command') as mock_run:
|
||||
record = await jobs.wait_for_job(job_id)
|
||||
finally:
|
||||
await wrol_mode_fixture(False)
|
||||
|
||||
assert record['status'] == jobs.FAILED, record
|
||||
assert 'WROL Mode' in record['error']
|
||||
mock_run.assert_not_called()
|
||||
assert video_path.read_bytes() == before
|
||||
|
|
|
|||
|
|
@ -115,3 +115,21 @@ async def test_video_update_api(async_client, test_session, video_factory, wrol_
|
|||
content=json.dumps({'title': 'Nope'}))
|
||||
assert response.status == 403
|
||||
await wrol_mode_fixture(False)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_video_wrol_mode(test_session, video_factory, wrol_mode_fixture):
|
||||
"""The library function refuses in WROL Mode, not only the API route, so no caller can edit."""
|
||||
from wrolpi.errors import WROLModeEnabled
|
||||
video = video_factory(with_info_json={'title': 'Old'})
|
||||
test_session.commit()
|
||||
|
||||
await wrol_mode_fixture(True)
|
||||
try:
|
||||
with pytest.raises(WROLModeEnabled):
|
||||
update_video(video.file_group_id, title='Nope')
|
||||
finally:
|
||||
await wrol_mode_fixture(False)
|
||||
|
||||
test_session.expire_all()
|
||||
assert Video.find_by_file_group_id(test_session, video.file_group_id).file_group.title == 'Old'
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from typing import List, Optional, Tuple, Callable
|
|||
|
||||
from wrolpi.captions import FFMPEG_BIN
|
||||
from wrolpi.cmd import run_command
|
||||
from wrolpi.common import logger, get_relative_to_media_directory
|
||||
from wrolpi.common import logger, get_relative_to_media_directory, wrol_mode_enabled
|
||||
from wrolpi.db import get_db_session
|
||||
from wrolpi.jobs import register_job, get_current_job
|
||||
from wrolpi.vars import DEFAULT_FILE_PERMISSIONS, PYTEST
|
||||
|
|
@ -269,6 +269,10 @@ async def transcode_video_job(file_group_id: int, video_codec: Optional[str] = N
|
|||
from wrolpi.files.lib import get_mimetype
|
||||
from wrolpi.files.worker import file_worker
|
||||
|
||||
# The API refuses to queue in WROL Mode; this catches a Job queued before it was enabled.
|
||||
if wrol_mode_enabled():
|
||||
raise RuntimeError('Cannot transcode while WROL Mode is enabled')
|
||||
|
||||
validate_transcode_request(video_codec, audio_codec, container)
|
||||
|
||||
with get_db_session() as session:
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ def get_video(file_group_id: int) -> Video:
|
|||
return video
|
||||
|
||||
|
||||
@wrol_mode_check
|
||||
def update_video(file_group_id: int, title: str = None, description: str = None) -> Video:
|
||||
"""Edit a Video's details. The files are the source of truth, so the change is written to the
|
||||
Video's .info.json (created if missing) and the Video is validated again from it. Only the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue