mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-06-28 05:51:22 +02:00
Add endpoint for deleting raw packets of decrypted messages
This commit is contained in:
@@ -801,6 +801,13 @@ class RawPacketRepository:
|
||||
await db.conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
@staticmethod
|
||||
async def purge_linked_to_messages() -> int:
|
||||
"""Delete raw packets that are already linked to a stored message."""
|
||||
cursor = await db.conn.execute("DELETE FROM raw_packets WHERE message_id IS NOT NULL")
|
||||
await db.conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
@staticmethod
|
||||
async def get_undecrypted_text_messages() -> list[tuple[int, bytes, int]]:
|
||||
"""Get all undecrypted TEXT_MESSAGE packets as (id, data, timestamp) tuples.
|
||||
|
||||
+26
-10
@@ -236,8 +236,12 @@ async def decrypt_historical_packets(
|
||||
|
||||
|
||||
class MaintenanceRequest(BaseModel):
|
||||
prune_undecrypted_days: int = Field(
|
||||
ge=1, description="Delete undecrypted packets older than this many days"
|
||||
prune_undecrypted_days: int | None = Field(
|
||||
default=None, ge=1, description="Delete undecrypted packets older than this many days"
|
||||
)
|
||||
purge_linked_raw_packets: bool = Field(
|
||||
default=False,
|
||||
description="Delete raw packets already linked to a stored message",
|
||||
)
|
||||
|
||||
|
||||
@@ -249,18 +253,30 @@ class MaintenanceResult(BaseModel):
|
||||
@router.post("/maintenance", response_model=MaintenanceResult)
|
||||
async def run_maintenance(request: MaintenanceRequest) -> MaintenanceResult:
|
||||
"""
|
||||
Clean up old undecrypted packets and reclaim disk space.
|
||||
Run packet maintenance tasks and reclaim disk space.
|
||||
|
||||
- Deletes undecrypted packets older than the specified number of days
|
||||
- Optionally deletes undecrypted packets older than the specified number of days
|
||||
- Optionally deletes raw packets already linked to stored messages
|
||||
- Runs VACUUM to reclaim disk space
|
||||
"""
|
||||
logger.info(
|
||||
"Running maintenance: pruning packets older than %d days", request.prune_undecrypted_days
|
||||
)
|
||||
deleted = 0
|
||||
|
||||
# Prune old undecrypted packets
|
||||
deleted = await RawPacketRepository.prune_old_undecrypted(request.prune_undecrypted_days)
|
||||
logger.info("Deleted %d old undecrypted packets", deleted)
|
||||
if request.prune_undecrypted_days is not None:
|
||||
logger.info(
|
||||
"Running maintenance: pruning undecrypted packets older than %d days",
|
||||
request.prune_undecrypted_days,
|
||||
)
|
||||
pruned_undecrypted = await RawPacketRepository.prune_old_undecrypted(
|
||||
request.prune_undecrypted_days
|
||||
)
|
||||
deleted += pruned_undecrypted
|
||||
logger.info("Deleted %d old undecrypted packets", pruned_undecrypted)
|
||||
|
||||
if request.purge_linked_raw_packets:
|
||||
logger.info("Running maintenance: purging raw packets linked to stored messages")
|
||||
purged_linked = await RawPacketRepository.purge_linked_to_messages()
|
||||
deleted += purged_linked
|
||||
logger.info("Deleted %d linked raw packets", purged_linked)
|
||||
|
||||
# Run VACUUM to reclaim space on a dedicated connection
|
||||
async with aiosqlite.connect(db.db_path) as vacuum_conn:
|
||||
|
||||
+9
-2
@@ -183,10 +183,17 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
}),
|
||||
runMaintenance: (pruneUndecryptedDays: number) =>
|
||||
runMaintenance: (options: { pruneUndecryptedDays?: number; purgeLinkedRawPackets?: boolean }) =>
|
||||
fetchJson<MaintenanceResult>('/packets/maintenance', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prune_undecrypted_days: pruneUndecryptedDays }),
|
||||
body: JSON.stringify({
|
||||
...(options.pruneUndecryptedDays !== undefined && {
|
||||
prune_undecrypted_days: options.pruneUndecryptedDays,
|
||||
}),
|
||||
...(options.purgeLinkedRawPackets !== undefined && {
|
||||
purge_linked_raw_packets: options.purgeLinkedRawPackets,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
// Read State
|
||||
|
||||
@@ -145,6 +145,7 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
// Database maintenance state
|
||||
const [retentionDays, setRetentionDays] = useState('14');
|
||||
const [cleaning, setCleaning] = useState(false);
|
||||
const [purgingDecryptedRaw, setPurgingDecryptedRaw] = useState(false);
|
||||
const [autoDecryptOnAdvert, setAutoDecryptOnAdvert] = useState(false);
|
||||
const [reopenLastConversation, setReopenLastConversation] = useState(
|
||||
getReopenLastConversationEnabled
|
||||
@@ -509,7 +510,7 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
setCleaning(true);
|
||||
|
||||
try {
|
||||
const result = await api.runMaintenance(days);
|
||||
const result = await api.runMaintenance({ pruneUndecryptedDays: days });
|
||||
toast.success('Database cleanup complete', {
|
||||
description: `Deleted ${result.packets_deleted} old packet${result.packets_deleted === 1 ? '' : 's'}`,
|
||||
});
|
||||
@@ -524,6 +525,25 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurgeDecryptedRawPackets = async () => {
|
||||
setPurgingDecryptedRaw(true);
|
||||
|
||||
try {
|
||||
const result = await api.runMaintenance({ purgeLinkedRawPackets: true });
|
||||
toast.success('Decrypted raw packets purged', {
|
||||
description: `Deleted ${result.packets_deleted} raw packet${result.packets_deleted === 1 ? '' : 's'}`,
|
||||
});
|
||||
await onHealthRefresh();
|
||||
} catch (err) {
|
||||
console.error('Failed to purge decrypted raw packets:', err);
|
||||
toast.error('Failed to purge decrypted raw packets', {
|
||||
description: err instanceof Error ? err.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setPurgingDecryptedRaw(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveDatabaseSettings = async () => {
|
||||
setBusySection('database');
|
||||
setSectionError(null);
|
||||
@@ -1046,6 +1066,27 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label>Purge Decrypted Raw Packets</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Deletes raw packet bytes for messages already stored in chat history. No displayed
|
||||
message data is lost. Only use this if you do not plan to run manual analytics or
|
||||
parsing on original packet bytes.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handlePurgeDecryptedRawPackets}
|
||||
disabled={purgingDecryptedRaw}
|
||||
className="w-full border-red-500/50 text-red-400 hover:bg-red-500/10"
|
||||
>
|
||||
{purgingDecryptedRaw
|
||||
? 'Purging Decrypted Raw Packets...'
|
||||
: 'Purge Decrypted Raw Packets'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label>DM Decryption</Label>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
LAST_VIEWED_CONVERSATION_KEY,
|
||||
REOPEN_LAST_CONVERSATION_KEY,
|
||||
} from '../utils/lastViewedConversation';
|
||||
import { api } from '../api';
|
||||
|
||||
const baseConfig: RadioConfig = {
|
||||
public_key: 'aa'.repeat(32),
|
||||
@@ -321,6 +322,22 @@ describe('SettingsModal', () => {
|
||||
expect(localStorage.getItem(LAST_VIEWED_CONVERSATION_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('purges decrypted raw packets via maintenance endpoint action', async () => {
|
||||
const runMaintenanceSpy = vi.spyOn(api, 'runMaintenance').mockResolvedValue({
|
||||
packets_deleted: 12,
|
||||
vacuumed: true,
|
||||
});
|
||||
|
||||
renderModal();
|
||||
openDatabaseSection();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Purge Decrypted Raw Packets' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(runMaintenanceSpy).toHaveBeenCalledWith({ purgeLinkedRawPackets: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('renders statistics section with fetched data', async () => {
|
||||
const mockStats: StatisticsResponse = {
|
||||
busiest_channels_24h: [
|
||||
|
||||
@@ -887,6 +887,23 @@ class TestRawPacketRepository:
|
||||
deleted = await RawPacketRepository.prune_old_undecrypted(10)
|
||||
assert deleted == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_purge_linked_to_messages_deletes_only_linked_packets(self, test_db):
|
||||
"""Purge linked raw packets removes only rows with a message_id."""
|
||||
ts = int(time.time())
|
||||
linked_1, _ = await RawPacketRepository.create(b"\x01\x02\x03", ts)
|
||||
linked_2, _ = await RawPacketRepository.create(b"\x04\x05\x06", ts)
|
||||
await RawPacketRepository.mark_decrypted(linked_1, 101)
|
||||
await RawPacketRepository.mark_decrypted(linked_2, 102)
|
||||
|
||||
await RawPacketRepository.create(b"\x07\x08\x09", ts) # undecrypted, should remain
|
||||
|
||||
deleted = await RawPacketRepository.purge_linked_to_messages()
|
||||
assert deleted == 2
|
||||
|
||||
remaining = await RawPacketRepository.get_undecrypted_count()
|
||||
assert remaining == 1
|
||||
|
||||
|
||||
class TestMaintenanceEndpoint:
|
||||
"""Test database maintenance endpoint."""
|
||||
@@ -909,6 +926,23 @@ class TestMaintenanceEndpoint:
|
||||
assert result.packets_deleted == 2
|
||||
assert result.vacuumed is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maintenance_can_purge_linked_raw_packets(self, test_db):
|
||||
"""Maintenance endpoint can purge raw packets linked to messages."""
|
||||
from app.routers.packets import MaintenanceRequest, run_maintenance
|
||||
|
||||
ts = int(time.time())
|
||||
linked_1, _ = await RawPacketRepository.create(b"\x0A\x0B\x0C", ts)
|
||||
linked_2, _ = await RawPacketRepository.create(b"\x0D\x0E\x0F", ts)
|
||||
await RawPacketRepository.mark_decrypted(linked_1, 201)
|
||||
await RawPacketRepository.mark_decrypted(linked_2, 202)
|
||||
|
||||
request = MaintenanceRequest(purge_linked_raw_packets=True)
|
||||
result = await run_maintenance(request)
|
||||
|
||||
assert result.packets_deleted == 2
|
||||
assert result.vacuumed is True
|
||||
|
||||
|
||||
class TestHealthEndpointDatabaseSize:
|
||||
"""Test database size reporting in health endpoint."""
|
||||
|
||||
Reference in New Issue
Block a user