From e24990aae22ba6b2b1b6929df2ae2ffc254985d5 Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 15 Dec 2022 11:48:46 -0700 Subject: [PATCH] Creating File Statistics page. --- app/src/api.js | 15 ++++++++ app/src/components/Apps.js | 67 +++++++++++++++++++++++++++++++++-- app/src/components/Nav.js | 1 + app/src/hooks/customHooks.js | 20 +++++++++++ wrolpi/files/api.py | 9 +++++ wrolpi/files/lib.py | 23 ++++++++++++ wrolpi/files/test/test_api.py | 32 +++++++++++++++++ 7 files changed, 165 insertions(+), 2 deletions(-) diff --git a/app/src/api.js b/app/src/api.js index 7f32a504..ed1d8db6 100644 --- a/app/src/api.js +++ b/app/src/api.js @@ -661,3 +661,18 @@ export async function getAPIStatus() { let response = await apiGet(`${API_URI}/echo`); return response.status === 200; } + +export async function getFileStatistics() { + let response = await apiGet(`${API_URI}/files/statistics`); + if (response.status === 200) { + const contents = await response.json(); + return contents['statistics']; + } else { + toast({ + type: 'error', + title: 'Error!', + description: 'Unable to get file statistics', + time: 5000, + }) + } +} diff --git a/app/src/components/Apps.js b/app/src/components/Apps.js index 04ce221f..efc4a334 100644 --- a/app/src/components/Apps.js +++ b/app/src/components/Apps.js @@ -1,11 +1,12 @@ import React, {useContext} from 'react'; -import {Divider, SegmentGroup} from "semantic-ui-react"; +import {Divider, SegmentGroup, StatisticLabel, StatisticValue} from "semantic-ui-react"; import {Route, Routes} from "react-router-dom"; import "../static/wrolpi.css"; import {decryptOTP, encryptOTP} from "../api"; import {PageContainer, useTitle} from "./Common"; import {ThemeContext} from "../contexts/contexts"; -import {Button, Header, Segment, TextArea} from "./Theme"; +import {Button, Header, Loader, Segment, Statistic, StatisticGroup, TextArea} from "./Theme"; +import {useFileStatistics} from "../hooks/customHooks"; class Encrypt extends React.Component { constructor(props) { @@ -145,10 +146,72 @@ function OTP() { } +function FileStatistics() { + useTitle('File Statistics'); + + const {s} = useContext(ThemeContext); + + const {statistics} = useFileStatistics(); + + let body = ; + + if (statistics === undefined) { + body =

Failed to fetch statistics

; + } + + if (statistics !== undefined && statistics !== null) { + const {total_count, video_count, pdf_count, ebook_count, archive_count, image_count, zip_count} = statistics; + body = <> + + + {total_count} + All Files + + + + + {video_count} + Videos + + + {pdf_count} + PDFs + + + {ebook_count} + eBooks + + + {archive_count} + Archives + + + {image_count} + Images + + + + + {zip_count} + ZIP + + + ; + } + + return <> +
File Statistics
+ + {body} + + +} + export function AppsRoute(props) { return ( }/> + }/> ) } diff --git a/app/src/components/Nav.js b/app/src/components/Nav.js index 15a0ee5c..1a4dccef 100644 --- a/app/src/components/Nav.js +++ b/app/src/components/Nav.js @@ -16,6 +16,7 @@ const links = [ { key: 'apps', text: 'Apps', links: [ {to: '/apps/otp', text: 'One Time Pad', end: true}, + {to: '/apps/file_statistics', text: 'File Statistics', end: true}, ] }, ]; diff --git a/app/src/hooks/customHooks.js b/app/src/hooks/customHooks.js index 86f2b28c..dbcbc961 100644 --- a/app/src/hooks/customHooks.js +++ b/app/src/hooks/customHooks.js @@ -10,6 +10,7 @@ import { getDownloaders, getDownloads, getFiles, + getFileStatistics, getInventory, getSettings, getStatistics, @@ -718,3 +719,22 @@ export const useInventory = (inventoryId) => { return {byCategory, bySubcategory, byName, fetchInventory} } + +export const useFileStatistics = () => { + const [statistics, setStatistics] = useState(); + + const fetchFileStatistics = async () => { + try { + const statistics = await getFileStatistics(); + setStatistics(statistics || undefined); + } catch (e) { + console.error(e); + } + } + + useEffect(() => { + fetchFileStatistics(); + }, []); + + return {statistics}; +} diff --git a/wrolpi/files/api.py b/wrolpi/files/api.py index 0398856d..5cbe33e3 100644 --- a/wrolpi/files/api.py +++ b/wrolpi/files/api.py @@ -135,3 +135,12 @@ def directories(_, body: schema.DirectoriesRequest): body = {'directories': dirs, 'exists': path.exists(), 'is_dir': path.is_dir(), 'is_file': path.is_file()} return response.json(body) + + +@bp.get('/statistics') +@openapi.definition( + summary='Get summary statistics of all files', +) +async def get_statistics(_): + statistics = await lib.get_file_statistics() + return json_response({'statistics': statistics}) diff --git a/wrolpi/files/lib.py b/wrolpi/files/lib.py index fb03901b..5b0a1c9c 100644 --- a/wrolpi/files/lib.py +++ b/wrolpi/files/lib.py @@ -621,3 +621,26 @@ def split_file_name_words(name: str) -> List[str]: except Exception as e: logger.error(f'Failed to split filename into words: {name}', exc_info=e) return [name] + + +async def get_file_statistics(): + with get_db_curs() as curs: + curs.execute(''' + SELECT + COUNT(path) AS "total_count", + COUNT(path) FILTER (WHERE file.mimetype LIKE 'video/%') AS "video_count", + COUNT(path) FILTER (WHERE file.mimetype = 'application/pdf') AS "pdf_count", + COUNT(path) FILTER (WHERE file.mimetype LIKE 'image/%' AND file.associated = FALSE) AS "image_count", + COUNT(path) FILTER (WHERE file.mimetype = 'application/zip') AS "zip_count" + FROM + file + ''') + statistics = dict(curs.fetchall()[0]) + + curs.execute('SELECT COUNT(*) FROM archive') + statistics['archive_count'] = curs.fetchall()[0][0] + + curs.execute('SELECT COUNT(*) FROM ebook') + statistics['ebook_count'] = curs.fetchall()[0][0] + + return statistics diff --git a/wrolpi/files/test/test_api.py b/wrolpi/files/test/test_api.py index b9158e5c..811d2743 100644 --- a/wrolpi/files/test/test_api.py +++ b/wrolpi/files/test/test_api.py @@ -242,3 +242,35 @@ def test_refresh_files_list(test_session, test_client, make_files_structure): request, response = test_client.post('/api/files/refresh/list', content=content) assert response.status_code == HTTPStatus.NO_CONTENT assert test_session.query(File).count() == 1 + + +def test_file_statistics(test_session, test_client, example_pdf, example_mobi, example_epub, video_file): + """A summary of File statistics can be fetched.""" + # Statistics can be fetched while empty. + request, response = test_client.get('/api/files/statistics') + assert response.status_code == HTTPStatus.OK + assert response.json == { + 'statistics': { + 'archive_count': 0, + 'ebook_count': 0, + 'image_count': 0, + 'pdf_count': 0, + 'total_count': 0, + 'video_count': 0, + 'zip_count': 0}, + } + + test_client.post('/api/files/refresh') + + request, response = test_client.get('/api/files/statistics') + assert response.status_code == HTTPStatus.OK + assert response.json == { + 'statistics': { + 'archive_count': 0, + 'ebook_count': 2, + 'image_count': 0, + 'pdf_count': 1, + 'total_count': 5, + 'video_count': 1, + 'zip_count': 0}, + }