From 3aa7b47d613be57b2a0c35dde0a6a053f07bc655 Mon Sep 17 00:00:00 2001 From: Roland Date: Tue, 15 Aug 2023 13:47:21 -0600 Subject: [PATCH] Directories can be deleted in /api/files/delete. --- wrolpi/files/api.py | 28 +++--------------------- wrolpi/files/lib.py | 10 +++++++-- wrolpi/files/test/test_api.py | 40 ++++++++++++----------------------- wrolpi/files/test/test_lib.py | 26 ++++++++++++----------- 4 files changed, 39 insertions(+), 65 deletions(-) diff --git a/wrolpi/files/api.py b/wrolpi/files/api.py index 48375142..a7d8fab5 100644 --- a/wrolpi/files/api.py +++ b/wrolpi/files/api.py @@ -1,5 +1,4 @@ import pathlib -import shutil from http import HTTPStatus from multiprocessing import Manager from typing import List @@ -48,7 +47,8 @@ async def get_file(_: Request, body: schema.FileRequest): @bp.post('/delete') @openapi.definition( - summary='Delete a single file. Returns an error if WROL Mode is enabled.', + summary='Delete files or directories. Directories are deleted recursively.' + ' Returns an error if WROL Mode is enabled.', body=schema.DeleteRequest, ) @validate(schema.DeleteRequest) @@ -56,7 +56,7 @@ async def delete_file(_: Request, body: schema.DeleteRequest): paths = [i for i in body.paths if i] if not paths: raise InvalidFile('paths cannot be empty') - lib.delete(*paths) + await lib.delete(*paths) return response.empty() @@ -251,28 +251,6 @@ async def post_rename(_: Request, body: schema.Rename): return response.empty(HTTPStatus.NO_CONTENT) -@bp.post('/delete_directory') -@openapi.definition( - summary='Delete a directory in the media directory.', - body=schema.Directory, -) -@validate(schema.Directory) -@wrol_mode_check -async def delete_directory(_: Request, body: schema.Directory): - path = get_media_directory() / body.path - if path.is_dir(): - lib.delete_directory(path, recursive=True) - else: - raise FileNotFoundError(f'No directory found: {path}') - - if PYTEST: - await lib.refresh_files([path, ]) - else: - background_task(lib.refresh_files([path, ])) - - return response.empty() - - @bp.post('/tag') @validate(schema.TagFileGroupPost) async def post_tag_file_group(_, body: schema.TagFileGroupPost): diff --git a/wrolpi/files/lib.py b/wrolpi/files/lib.py index e1fc7650..9b6605d8 100644 --- a/wrolpi/files/lib.py +++ b/wrolpi/files/lib.py @@ -134,7 +134,7 @@ def list_directories_contents(directories_: List[str]) -> Dict: @wrol_mode_check -def delete(*paths: Union[str, pathlib.Path]): +async def delete(*paths: Union[str, pathlib.Path]): """ Delete a file or directory in the media directory. @@ -166,10 +166,16 @@ def delete(*paths: Union[str, pathlib.Path]): raise FileGroupIsTagged(f"Cannot delete {file_group} because it is tagged") for path in paths: if path.is_dir(): - delete_directory(path) + delete_directory(path, recursive=True) else: path.unlink() + coro = refresh_files(paths) + if PYTEST: + await coro + else: + background_task(coro) + FILE_NAME_REGEX = re.compile(r'[_ .]') diff --git a/wrolpi/files/test/test_api.py b/wrolpi/files/test/test_api.py index 64464d83..70eb3419 100644 --- a/wrolpi/files/test/test_api.py +++ b/wrolpi/files/test/test_api.py @@ -4,7 +4,6 @@ from unittest import mock import pytest -from wrolpi.errors import WROLModeEnabled from wrolpi.files import lib from wrolpi.files.models import FileGroup from wrolpi.tags import TagFile @@ -113,7 +112,7 @@ def test_list_files_api(test_session, test_client, make_files_structure, test_di def test_delete_file(test_session, test_client, make_files_structure, test_directory): - files = ['bar.txt', 'baz/'] + files = ['bar.txt', 'baz/', 'foo'] make_files_structure(files) # Delete a file. @@ -503,8 +502,8 @@ async def test_directory_crud(test_session, test_async_client, test_directory, a # Deletion is recursive. assert (test_directory / 'foo/baz/asdf.txt').is_file() - content = dict(path='foo/baz') - request, response = await test_async_client.post('/api/files/delete_directory', content=json.dumps(content)) + content = dict(paths=['foo/baz', ]) + request, response = await test_async_client.post('/api/files/delete', content=json.dumps(content)) assert response.status_code == HTTPStatus.NO_CONTENT assert not (test_directory / 'foo/baz').exists() assert not (test_directory / 'foo/baz/asdf.txt').exists() @@ -573,27 +572,16 @@ async def test_rename_directory(test_session, test_directory, make_files_structu @pytest.mark.asyncio -async def test_delete_directory_tagged(test_session, test_directory, make_files_structure, test_async_client, - tag_factory, insert_file_group): - """Cannot delete a directory if it contained a tagged FileGroup.""" - bar, = make_files_structure({ - 'foo/bar.txt': 'bar' - }, file_groups=True, session=test_session) - bar: FileGroup - tag = tag_factory() - bar.add_tag(tag) - test_session.commit() +async def test_delete_directory_recursive(test_session, test_directory, make_files_structure, test_async_client, + assert_files): + make_files_structure(['dir/foo', 'dir/bar', 'empty']) + await lib.refresh_files() + assert test_session.query(FileGroup).count() == 3 - # Cannot deleted tagged file. - content = dict(path=str(bar.primary_path.parent)) - request, response = await test_async_client.post('/api/files/delete_directory', content=json.dumps(content)) - assert response.status_code == HTTPStatus.CONFLICT - assert (test_directory / 'foo').is_dir() - - # Remove tag, directory is deleted recursively. - bar.remove_tag(tag) - test_session.commit() - content = dict(path=str(bar.primary_path.parent)) - request, response = await test_async_client.post('/api/files/delete_directory', content=json.dumps(content)) + content = {'paths': ['dir/', ]} + request, response = await test_async_client.post('/api/files/delete', content=json.dumps(content)) assert response.status_code == HTTPStatus.NO_CONTENT - assert not (test_directory / 'foo').exists() + assert test_session.query(FileGroup).count() == 1 + + empty = test_session.query(FileGroup).one() + assert empty.primary_path.name == 'empty' diff --git a/wrolpi/files/test/test_lib.py b/wrolpi/files/test/test_lib.py index e27795c5..75b42378 100644 --- a/wrolpi/files/test/test_lib.py +++ b/wrolpi/files/test/test_lib.py @@ -21,7 +21,8 @@ from wrolpi.tags import TagFile from wrolpi.vars import PROJECT_DIR -def test_delete(test_session, make_files_structure, test_directory): +@pytest.mark.asyncio +async def test_delete(test_session, make_files_structure, test_directory): """ File in the media directory can be deleted. """ @@ -31,34 +32,35 @@ def test_delete(test_session, make_files_structure, test_directory): 'baz/', ]) - lib.delete('bar.txt') + await lib.delete('bar.txt') assert (test_directory / 'archives/foo.txt').is_file() assert not (test_directory / 'bar.txt').exists() assert (test_directory / 'baz').is_dir() - lib.delete('archives/foo.txt') + await lib.delete('archives/foo.txt') assert not (test_directory / 'archives/foo.txt').exists() assert not (test_directory / 'bar.txt').exists() # Can also delete directories. - lib.delete('baz') + await lib.delete('baz') assert not (test_directory / 'baz').exists() with pytest.raises(InvalidFile): - lib.delete('does not exist') + await lib.delete('does not exist') # Cannot delete the media directory. with pytest.raises(InvalidFile): - lib.delete('.') + await lib.delete('.') -def test_delete_link(test_session, test_directory): +@pytest.mark.asyncio +async def test_delete_link(test_session, test_directory): """Links can be deleted.""" foo, bar = test_directory / 'foo', test_directory / 'bar' foo.touch() bar.symlink_to(foo) - lib.delete(bar) + await lib.delete(bar) @pytest.mark.asyncio @@ -74,11 +76,11 @@ async def test_delete_tagged(test_session, make_files_structure, tag_factory, vi # Neither file can be deleted. with pytest.raises(FileGroupIsTagged): - lib.delete('foo/bar.txt') + await lib.delete('foo/bar.txt') with pytest.raises(FileGroupIsTagged): - lib.delete('foo/bar.mp4') + await lib.delete('foo/bar.mp4') with pytest.raises(FileGroupIsTagged): - lib.delete('foo') + await lib.delete('foo') @pytest.mark.asyncio @@ -87,7 +89,7 @@ async def test_delete_nested(test_session, make_files_structure): make_files_structure(['foo/bar']) with pytest.raises(InvalidFile): - lib.delete('foo', 'foo/bar') + await lib.delete('foo', 'foo/bar') @pytest.mark.parametrize(