A new database is refreshed on startup (#827)

Download and Upload are disabled until a full refresh completes, and a
fresh install never had one until the user found the refresh link.
Startup now queues it, and the dashboard says why Download and Upload
are disabled meanwhile.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lrnselfreliance 2026-10-13 16:07:04 -06:00 • committed by GitHub
parent 295b3ccfc8
commit 1554dfb268
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 66 additions and 5 deletions

View file

@ -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.

View file

@ -252,6 +252,8 @@ export function Getters() {
</div>
<span className='wrolpi-or-label'>Or</span>
</div>
{status?.flags && gettersDisabled &&
<p>Download and Upload are available after your files are refreshed.</p>}
</Panel>;
let getterModal;

View file

@ -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(
<StatusContext.Provider value={{status}}>
<Getters/>
@ -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

View file

@ -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.

View file

@ -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

View file

@ -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.