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.