diff --git a/INSTALL.md b/INSTALL.md index ce52e892..15ae55f5 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -94,8 +94,8 @@ These steps are the same for every install. 3. **Trust the certificate.** Your browser will warn about the HTTPS certificate because it was made by your WROLPi, not by a public authority. Download https://wrolpi.local/ca.crt and trust it on each device you use; see *Help > Certificates* on your WROLPi for per-device instructions. -4. **Refresh your files.** If you copied files onto the drive, the dashboard will ask you to refresh so WROLPi can find - them. Your library looks empty until the refresh completes. +4. **Wait for the first refresh.** WROLPi refreshes all your files when it first starts so it can find anything already + on the drive. Your library looks empty, and Download and Upload are disabled, until the refresh completes. 5. Start adding content: download a video channel, archive a web page, or add a Zim (Wikipedia) file. The *Help* page on your WROLPi explains each module. diff --git a/app/src/DashboardPage.js b/app/src/DashboardPage.js index b5b9aac7..1ab785ab 100644 --- a/app/src/DashboardPage.js +++ b/app/src/DashboardPage.js @@ -252,6 +252,8 @@ export function Getters() { Or + {status?.flags && gettersDisabled && +

Download and Upload are available after your files are refreshed.

} ; let getterModal; diff --git a/app/src/DashboardPage.test.js b/app/src/DashboardPage.test.js index 1d75414c..26d5bdb7 100644 --- a/app/src/DashboardPage.test.js +++ b/app/src/DashboardPage.test.js @@ -21,11 +21,11 @@ jest.mock('./hooks/customHooks', () => ({ useWROLMode: () => false, })); -function renderAtUrl(url) { +function renderAtUrl(url, flags = {refresh_complete: true}) { // test-utils' render() wraps content in a BrowserRouter (not MemoryRouter), // so point the jsdom location at the desired URL before rendering. window.history.pushState({}, '', url); - const status = {flags: {refresh_complete: true}}; + const status = {flags}; return render( @@ -67,6 +67,25 @@ describe('DashboardPage Getters deep-link', () => { }); }); +describe('DashboardPage Getters before the first refresh', () => { + const note = /available after your files are refreshed/i; + + it('explains why Download and Upload are disabled', () => { + renderAtUrl('/', {refresh_complete: false}); + expect(screen.getByText(note)).toBeInTheDocument(); + }); + + it('says nothing once the refresh has completed', () => { + renderAtUrl('/'); + expect(screen.queryByText(note)).toBeNull(); + }); + + it('says nothing before the status has loaded', () => { + renderAtUrl('/', null); + expect(screen.queryByText(note)).toBeNull(); + }); +}); + // The DownloadMenu's seed-object construction (joining initialUrls with // newlines into the `download` prop's `urls` field) is exercised in the // browser via manual verification, not unit-tested here — reaching into the diff --git a/main.py b/main.py index 5fdf94ea..e29d49ff 100755 --- a/main.py +++ b/main.py @@ -310,6 +310,8 @@ async def start_single_tasks(app: Sanic): if flags.refresh_complete.is_set(): # Set all downloads to new. download_manager.retry_downloads_on_startup() + else: + file_worker.queue_first_refresh() # Hotspot/throttle on startup are now handled by the Controller service. diff --git a/wrolpi/files/test/test_worker.py b/wrolpi/files/test/test_worker.py index 8ef1ae08..8c585617 100644 --- a/wrolpi/files/test/test_worker.py +++ b/wrolpi/files/test/test_worker.py @@ -1949,3 +1949,29 @@ async def test_directory_refresh_deletes_stale_ignored_file_groups( test_session.expire_all() primary_paths = {str(i.primary_path) for i in test_session.query(FileGroup)} assert primary_paths == {str(video)} + + +@pytest.mark.asyncio +async def test_queue_first_refresh(async_client, test_session, test_directory, make_files_structure, + await_background_tasks, flags_lock): + """A new database is refreshed without the user asking, so a fresh WROLPi can download right away.""" + make_files_structure(['docs/file1.txt']) + flags.refresh_complete.clear() + + assert file_worker.queue_first_refresh() is not None + await await_background_tasks() + + assert flags.refresh_complete.is_set() is True + assert test_session.query(FileGroup).count() == 1 + + +@pytest.mark.asyncio +async def test_queue_first_refresh_skipped(async_client, test_session, test_directory, wrol_mode_fixture, + flags_lock): + """Only the first refresh is automatic, and WROL Mode never refreshes.""" + flags.refresh_complete.set() + assert file_worker.queue_first_refresh() is None + + flags.refresh_complete.clear() + await wrol_mode_fixture(True) + assert file_worker.queue_first_refresh() is None diff --git a/wrolpi/files/worker.py b/wrolpi/files/worker.py index c676adb5..b33ccb0f 100644 --- a/wrolpi/files/worker.py +++ b/wrolpi/files/worker.py @@ -21,7 +21,7 @@ from sqlalchemy import text, or_ from wrolpi import flags from wrolpi.common import apply_modelers, apply_refresh_cleanup -from wrolpi.common import get_media_directory, logger +from wrolpi.common import get_media_directory, logger, wrol_mode_enabled from wrolpi.db import get_db_session, get_db_curs from wrolpi.events import Events from wrolpi.vars import PYTEST @@ -254,6 +254,18 @@ class FileWorker(FileMoveMixin, FileReorganizeMixin): self.public_queue.put_nowait(task) return job_id + def queue_first_refresh(self) -> str | None: + """Queue a refresh of the entire media directory if one has never completed for this database (a fresh + install, or a rebuilt database). Downloads and uploads are disabled until it completes. + + Returns: + The job_id, or None if no refresh was queued. + """ + if flags.refresh_complete.is_set() or wrol_mode_enabled(): + return None + logger.info('Refreshing all files because no refresh has completed') + return self.queue_refresh([get_media_directory()]) + def queue_move(self, destination: pathlib.Path, sources: list[pathlib.Path]) -> str: """Queue a move task for background processing.